headroom/tests/test_transforms/test_code_compressor.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

2213 lines
78 KiB
Python
Raw Permalink Normal View History

"""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
from unittest.mock import patch
import pytest
fix(code-compressor): recover valid Python rewrites after local syntax rejection (#2202) ## Description A compile-invalid Python definition rewrite currently makes `CodeAwareCompressor` discard every otherwise valid rewrite in the file and return the original source at 0 percent reduction. The existing whole-file safety guard stays in place, while a Python-only recovery replay now preserves the rejected definition and keeps independent valid compression. The recovery reuses the current Python validation authority in `ast.parse()` plus `compile()`, runs only after the first assembled module already fails `_verify_syntax()`, and stays out of non-Python paths. Closes #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 - Added a Python-only recovery replay after the first assembled module fails syntax validation. - Preserved only the invalid function or class rewrite while allowing independent valid definitions to remain compressed. - Kept the existing whole-file syntax guard and original-source fallback as the terminal safety check. - Added focused invalid-node, valid-modern-syntax, and fail-safe coverage. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_invalid_node_falls_back_locally tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_does_not_block_valid_modern_syntax tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_still_returns_original_when_all_candidates_invalid tests/test_transforms/test_code_compressor.py::TestEdgeCases::test_syntax_errors_in_input tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_ast_preserves_structure_for_python -v`) - [x] Linting passes (`uv run ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_invalid_node_falls_back_locally tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_does_not_block_valid_modern_syntax tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_still_returns_original_when_all_candidates_invalid tests/test_transforms/test_code_compressor.py::TestEdgeCases::test_syntax_errors_in_input tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_ast_preserves_structure_for_python -v 5 passed in 0.34s uv run ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py All checks passed! uv run ruff format headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py --check 2 files already formatted ``` ## Real Behavior Proof - Environment: Windows, synced `uv` environment with `dev` and `code` extras installed - Exact command / steps: run the focused invalid-node regression through public `compress(..., language="python")` - Observed result: `1 passed in 0.19s`; the invalid candidate stays original, the neighboring valid candidate remains compressed, and `headroom-PR-TARGET-1233-PROOF.md` records the base `ratio=1.0` whole-file rollback against the fixed head behavior. - Not tested: the stale future-import mismatch discussed in the old issue comment, already covered on current main ## 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 - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - The stale future-import comment on #1233 is not the live slice here; current main already validates Python with `compile()` and already covers that ordering case. - This fix keeps the existing whole-file fail-safe and does not broaden into cross-language recovery or new syntax models. - `CHANGELOG.md` remains unchanged because Headroom generates release notes from conventional commits. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 23:19:46 -04:00
import headroom.transforms.code_compressor as cc
from headroom.transforms.code_compressor import (
CodeAwareCompressor,
CodeCompressionResult,
CodeCompressorConfig,
CodeLanguage,
DocstringMode,
feat(code): add PHP support to CodeAwareCompressor (#2423) ## Description Adds PHP to `CodeAwareCompressor`, fixing #201. PHP was already *detected* as code (Magika labels in `headroom/compression/detector.py` include `php`, and the Rust `magika_detector.rs` lists it too) but there was no PHP `LangConfig`, so PHP content silently passed through uncompressed. This wires PHP through the tree-sitter compression path following the C# pattern (the most recently added, fully functional language — deliberately not the quarantined Perl path). A secondary detection bug is fixed along the way: PHP's `$variables` match Perl's prefilter regex, and the existing Perl-dominance guard in `detect_language` returned `UNKNOWN` for PHP files. An explicit `<?php` open tag — which no Perl source contains — now drops Perl from the candidate set before that guard runs. ## Type of Change - [ ] Bug fix - [x] New feature - [ ] Documentation update - [ ] Refactor - [ ] Other ## Changes Made - `headroom/transforms/code_compressor.py`: `CodeLanguage.PHP` + `phtml`/`php5`/`php7`/`php8` aliases; PHP `LangConfig` built from the actual tree-sitter-php grammar (node names verified by parsing samples): `namespace_use_declaration` imports, `function_definition`/`method_declaration` functions, `class_declaration`/`interface_declaration`/`trait_declaration` classes, `enum_declaration` types, `declaration_list` class bodies, `compound_statement` function bodies. `namespace_definition` maps to `package_node` so statement-scoped `namespace App;` hoists ahead of the `use` imports (required PHP ordering); the rare block-scoped `namespace A { }` form takes the same path and is preserved verbatim — valid output, just no compression inside the block. PHP prefilter regexes added; supported-languages error message updated; `<?php`-tag Perl disambiguation in `detect_language`. - `headroom/transforms/content_detector.py`: `php` entry in `_CODE_PATTERNS` so raw PHP classifies as `SOURCE_CODE` and reaches the code-aware route. - `tests/test_transforms/test_code_compressor.py`: new `TestPhpSupport` mirroring `TestCSharpSupport` — signatures preserved / bodies elided, `<?php` → `namespace` → `use` → declarations ordering, auto-detection despite the Perl sigil overlap, alias coercion, malformed passthrough. - `tests/test_code_compressor_language_alias.py`: `php` in the canonical list, `phtml` in the alias table. - `docs/content/docs/code-compression.mdx`: PHP added to the Tier 2 supported-languages row. No new dependency: `tree-sitter-language-pack` (the existing `[code]` extra) already ships the PHP grammar. No Rust changes needed. ## Testing - [x] New unit tests added and passing - [x] Full affected test suites pass locally **Test Output** ``` $ python -m pytest tests/test_transforms/test_code_compressor.py tests/test_code_compressor_language_alias.py -q ============================= 120 passed in 7.81s ============================= $ python -m pytest tests/test_transforms/ -q 3 failed, 443 passed # the 3 failures (kompress ONNX thread caps, kompress size gate, # text_crusher unicode parity) reproduce identically on a clean # upstream/main checkout in this environment — pre-existing local # ONNX runtime quirks, unrelated to this change $ ruff check . (0.15.17, CI-pinned) → All checks passed! | ruff format --check → clean $ mypy headroom/transforms/code_compressor.py headroom/transforms/content_detector.py --ignore-missing-imports Success: no issues found in 2 source files ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.13, tree-sitter + tree-sitter-language-pack (<1.0) installed, branch `feat/201-php-code-compression` off `upstream/main`. - Exact command / steps: parsed PHP samples (namespaced class w/ methods, block-scoped namespace, mixed HTML+PHP) with `tree_sitter_language_pack.get_parser('php')` to verify every node name used in the config; then ran `CodeAwareCompressor().compress(php_code, language="php")` and `compress(php_code)` (auto-detection) on a 48-line realistic service class. - Observed result: explicit and auto-detected paths both return `language=CodeLanguage.PHP`, `compression_ratio=0.64`, `syntax_valid=True`; method bodies elided to `// [N lines omitted]` while `<?php`, `namespace`, `use` lines, class header, and all signatures are preserved verbatim in the original order. Before the detection fix, auto-detection returned `UNKNOWN` (Perl prefilter dominance) — reproduced and then verified fixed. - Not tested: exotic PHP shapes (heredoc-heavy code, attributes `#[...]` on methods, interleaved multi-`<?php ?>` HTML templates beyond the basic mixed case); these fall back to verbatim preservation via the uncaptured-node pass or malformed-passthrough, both of which are covered by tests for the simple cases. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-08-01 00:54:13 +02:00
coerce_language,
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
# Kompress fallback does NOT guarantee syntax validity
# If Kompress is unavailable, returns original (valid)
# If Kompress IS available, syntax_valid=False (cannot guarantee)
def test_fallback_preserves_structure(self, default_config):
"""Fallback compression preserves basic structure when no compressor available.
When both tree-sitter and Kompress are unavailable, the fallback
returns the original code unchanged - preserving all structure.
"""
with (
patch(
"headroom.transforms.code_compressor._check_tree_sitter_available",
return_value=False,
),
patch(
"headroom.transforms.kompress_compressor.is_kompress_available",
return_value=False,
),
):
compressor = CodeAwareCompressor(default_config)
code = generate_python_code(3)
result = compressor.compress(code)
# With no compressor available, original code is returned unchanged
# This preserves all imports and class/function signatures
assert "import os" in result.compressed
assert "def function_" in result.compressed
# Compression ratio should be 1.0 (no compression)
assert result.compression_ratio == 1.0
# =============================================================================
# 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
# syntax_valid requires tree-sitter; without it, validation is skipped
if is_tree_sitter_available():
assert result.syntax_valid is True
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):
fix(code): stop TS export duplication + comment displacement (#1906) ## Description `CodeAwareCompressor` (AST-based code compression, `headroom/transforms/code_compressor.py`) had two bugs in its structure-reassembly path, found while investigating a reported Go brace-duplication issue (the Go bug itself — `statement_list` row-range swallowing a block's closing brace — was already fixed on `main` in #1668; this PR fixes what was *actually* still broken): 1. **TS/JS `export` keyword duplication.** `export function foo() {}` / `export class Foo {}` compressed to `export export function foo() {}` — invalid syntax, silently discarded by `_verify_syntax`'s fallback (the caller never sees an error, compression just quietly no-ops). Root cause: `_compress_function_ast` / `_compress_class_ast` slice a node's source by **line**, not by byte offset, deliberately — to preserve leading indentation for definitions nested inside classes. But when a node shares its *first* line with a preceding sibling (the `export` keyword is a sibling of the function inside tree-sitter's `export_statement` node, not part of the function node itself), that line-based slice pulled the sibling's text in too. The `export_statement` handler then re-prepended the same `export` text on top, producing the duplicate. 2. **Doc-comment displacement (all languages).** A `/** ... */` or `//` doc comment directly above a top-level function/class/type got detached from its declaration during AST extraction and re-emitted in one cluster at the very end of the compressed output, instead of staying attached to what it documents. Root cause: doc comments are top-level *siblings* of the declaration they document, not children of it — the extractor didn't attach them to anything, so they fell through to a "leftover top-level code" bucket that gets flushed as a single block after all functions. Also tightens `test_actual_go_compression`, which — per its own comment — was written to *tolerate* the Go bug (`compression_ratio may be 1.0 if compression produces invalid syntax`) rather than catch it. Since the underlying Go bug is already fixed on `main`, this now asserts real compression (`compression_ratio < 1.0`), matching its JS/Python siblings. Closes #1905 ## 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 Two commits: the fix itself, then the tests that prove it — bisectable independently, both pass the full suite on their own. **Commit 1 — `fix(code):`** - `headroom/transforms/code_compressor.py`: add `_get_node_lines()` — line-based node slicing that still preserves indentation, but trims a preceding sibling's text from the first line when that prefix isn't pure whitespace (i.e. an `export` keyword sharing the line), so callers that re-add the sibling text themselves don't get a duplicate; used by `_compress_function_ast` and `_compress_class_ast`. - `headroom/transforms/code_compressor.py`: add `_get_leading_comment_text()` — walks a node's `prev_sibling` chain to collect contiguous doc-comment nodes immediately above it (no blank line in between) and returns them for the caller to prepend, also marking their byte ranges as captured so they aren't independently swept into the leftover top-level-code bucket; wired into every capture branch in `_extract_structure` (package, import, export statement, decorator, function, class, type). - `CHANGELOG.md`: added an entry under `### Fixed`. **Commit 2 — `test(code):`** - `tests/test_transforms/test_code_compressor.py`: `test_actual_go_compression` now asserts `compression_ratio < 1.0` instead of tolerating a 1.0 fallback. - `tests/test_code_aware_brace_comment_regressions.py` (new): 4 regression tests — TS `export` not duplicated + valid syntax, TS doc comments stay attached, Go doc comments stay attached, and a real-TS-compression parity test matching the existing JS/Python/Go "actual compression" tests. ## 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 $ ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py tests/test_code_aware_brace_comment_regressions.py All checks passed! $ ruff format --check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py tests/test_code_aware_brace_comment_regressions.py 3 files already formatted $ pytest tests/test_transforms/test_code_compressor.py tests/test_code_aware_regressions.py tests/test_code_aware_brace_comment_regressions.py -q 83 passed in 6.23s $ pytest -q # full suite 7912 passed, 5 failed, 442 skipped in 417.43s (0:06:57) # The 5 failures are pre-existing and unrelated: confirmed to fail identically # with this PR's changes stashed out (clean upstream/main checkout). # - test_wrap_marker_is_stale_when_pid_reused (PID-reuse detection, env-specific) # - test_read_cached_oauth_token_falls_back_to_gh_cli (leaks real local `gh` credentials) # - test_rtk_reader_returns_none_on_nonzero_exit / test_lean_ctx_reader_returns_none_on_failure_and_logs # (pass in isolation; fail only in full-suite order — pre-existing test-pollution, unrelated to code_compressor.py) # - test_parser_usable_in_thread_pool (test itself passes a str to parser.parse(), # which tree-sitter's binding has always required as bytes — a pre-existing test # bug unrelated to this change; separate fix in progress on another branch) $ mypy headroom Success: no issues found in 408 source files ``` ## Real Behavior Proof - Environment: macOS (Darwin 24.6.0), Python 3.14.5, headroom-ai dev checkout built via `uv sync --extra dev` + `maturin develop -m crates/headroom-py/Cargo.toml` (real `headroom._core` build, not mocked), `tree-sitter==0.25.2` / `tree-sitter-language-pack` per the pinned `[code]` extra. - Exact command / steps: ran `CodeAwareCompressor(CodeCompressorConfig(min_tokens_for_compression=10, enable_ccr=False)).compress(open("sdk/typescript/src/client.ts").read(), language="typescript")` identically against `git stash`-ed (pre-fix) and current (post-fix) trees; full snippet and additional samples below. - Observed result: `client.ts` (real 20KB SDK file in this repo) went from `compression_ratio=1.0` with a silent fallback (`export export class HeadroomClient` in the raw AST attempt, invalid syntax) to `compression_ratio=0.942`, `syntax_valid=True` — real compression, no duplication; full before/after table below. - Not tested: real-world repos beyond this repo's own SDK sample and the bundled benchmark fixture — broader corpus testing may follow as a comment on this PR. **Exact command, full snippet:** ```python from headroom.transforms.code_compressor import CodeAwareCompressor, CodeCompressorConfig compressor = CodeAwareCompressor(CodeCompressorConfig(min_tokens_for_compression=10, enable_ccr=False)) with open("sdk/typescript/src/client.ts") as f: code = f.read() result = compressor.compress(code, language="typescript") ``` **Observed result, before vs. after, real code:** | Sample | Before (main) | After (this fix) | |---|---|---| | `sdk/typescript/src/client.ts` (real 20KB SDK file, this repo) | `compression_ratio=1.0`, silent fallback — `export export class HeadroomClient` in the raw AST attempt, invalid syntax | `compression_ratio=0.942`, `syntax_valid=True` — real compression, no duplication | | TS fixture exercising both bugs (exported fn/class + doc comments) | `compression_ratio=1.0`, silent fallback | `compression_ratio=0.993`, `syntax_valid=True` | | `middleware/ratelimit.go` (bundled benchmark sample) | `compression_ratio=0.862`, `syntax_valid=True` — unaffected (Go bug already fixed on `main` by #1668) | `compression_ratio=0.862`, `syntax_valid=True` — unchanged, confirms no regression | | `generate_go_code(3)` (existing test fixture) | `compression_ratio=0.498` | `compression_ratio=0.498` — unchanged, confirms no regression | On code shaped to actually exercise elision (function bodies long enough to exceed `max_body_lines=5`), TypeScript compresses in line with other languages once the correctness bug stops blocking it entirely: | Language | Compression savings (synthetic fixture, ~10-line function bodies) | |---|---| | Python | 64.4% | | Go | 52.3% | | TypeScript | 49.0% | | JavaScript | 42.8% | (`client.ts`'s real-world 5.8% savings is lower than the synthetic TypeScript number above because most of its methods are ≤5 lines — under the elision threshold regardless of language — not because of a language-specific limitation.) ## 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 - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes The Go brace-duplication bug that motivated this investigation was already fixed on `main` (#1668, merged before this branch was based) — confirmed via the minimal repro and `ratelimit.go`, both compress cleanly with no duplicated braces. This PR fixes what was still actually broken: the TS/JS `export`-duplication bug and the doc-comment displacement bug (both present across languages), found empirically while verifying the original bug report against the current `main`. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-10 01:51:32 +08:00
"""Test actual compression of Go code."""
config = CodeCompressorConfig(
min_tokens_for_compression=10,
enable_ccr=False,
)
compressor = CodeAwareCompressor(config)
code = generate_go_code(3)
result = compressor.compress(code, language="go")
fix(code): stop TS export duplication + comment displacement (#1906) ## Description `CodeAwareCompressor` (AST-based code compression, `headroom/transforms/code_compressor.py`) had two bugs in its structure-reassembly path, found while investigating a reported Go brace-duplication issue (the Go bug itself — `statement_list` row-range swallowing a block's closing brace — was already fixed on `main` in #1668; this PR fixes what was *actually* still broken): 1. **TS/JS `export` keyword duplication.** `export function foo() {}` / `export class Foo {}` compressed to `export export function foo() {}` — invalid syntax, silently discarded by `_verify_syntax`'s fallback (the caller never sees an error, compression just quietly no-ops). Root cause: `_compress_function_ast` / `_compress_class_ast` slice a node's source by **line**, not by byte offset, deliberately — to preserve leading indentation for definitions nested inside classes. But when a node shares its *first* line with a preceding sibling (the `export` keyword is a sibling of the function inside tree-sitter's `export_statement` node, not part of the function node itself), that line-based slice pulled the sibling's text in too. The `export_statement` handler then re-prepended the same `export` text on top, producing the duplicate. 2. **Doc-comment displacement (all languages).** A `/** ... */` or `//` doc comment directly above a top-level function/class/type got detached from its declaration during AST extraction and re-emitted in one cluster at the very end of the compressed output, instead of staying attached to what it documents. Root cause: doc comments are top-level *siblings* of the declaration they document, not children of it — the extractor didn't attach them to anything, so they fell through to a "leftover top-level code" bucket that gets flushed as a single block after all functions. Also tightens `test_actual_go_compression`, which — per its own comment — was written to *tolerate* the Go bug (`compression_ratio may be 1.0 if compression produces invalid syntax`) rather than catch it. Since the underlying Go bug is already fixed on `main`, this now asserts real compression (`compression_ratio < 1.0`), matching its JS/Python siblings. Closes #1905 ## 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 Two commits: the fix itself, then the tests that prove it — bisectable independently, both pass the full suite on their own. **Commit 1 — `fix(code):`** - `headroom/transforms/code_compressor.py`: add `_get_node_lines()` — line-based node slicing that still preserves indentation, but trims a preceding sibling's text from the first line when that prefix isn't pure whitespace (i.e. an `export` keyword sharing the line), so callers that re-add the sibling text themselves don't get a duplicate; used by `_compress_function_ast` and `_compress_class_ast`. - `headroom/transforms/code_compressor.py`: add `_get_leading_comment_text()` — walks a node's `prev_sibling` chain to collect contiguous doc-comment nodes immediately above it (no blank line in between) and returns them for the caller to prepend, also marking their byte ranges as captured so they aren't independently swept into the leftover top-level-code bucket; wired into every capture branch in `_extract_structure` (package, import, export statement, decorator, function, class, type). - `CHANGELOG.md`: added an entry under `### Fixed`. **Commit 2 — `test(code):`** - `tests/test_transforms/test_code_compressor.py`: `test_actual_go_compression` now asserts `compression_ratio < 1.0` instead of tolerating a 1.0 fallback. - `tests/test_code_aware_brace_comment_regressions.py` (new): 4 regression tests — TS `export` not duplicated + valid syntax, TS doc comments stay attached, Go doc comments stay attached, and a real-TS-compression parity test matching the existing JS/Python/Go "actual compression" tests. ## 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 $ ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py tests/test_code_aware_brace_comment_regressions.py All checks passed! $ ruff format --check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py tests/test_code_aware_brace_comment_regressions.py 3 files already formatted $ pytest tests/test_transforms/test_code_compressor.py tests/test_code_aware_regressions.py tests/test_code_aware_brace_comment_regressions.py -q 83 passed in 6.23s $ pytest -q # full suite 7912 passed, 5 failed, 442 skipped in 417.43s (0:06:57) # The 5 failures are pre-existing and unrelated: confirmed to fail identically # with this PR's changes stashed out (clean upstream/main checkout). # - test_wrap_marker_is_stale_when_pid_reused (PID-reuse detection, env-specific) # - test_read_cached_oauth_token_falls_back_to_gh_cli (leaks real local `gh` credentials) # - test_rtk_reader_returns_none_on_nonzero_exit / test_lean_ctx_reader_returns_none_on_failure_and_logs # (pass in isolation; fail only in full-suite order — pre-existing test-pollution, unrelated to code_compressor.py) # - test_parser_usable_in_thread_pool (test itself passes a str to parser.parse(), # which tree-sitter's binding has always required as bytes — a pre-existing test # bug unrelated to this change; separate fix in progress on another branch) $ mypy headroom Success: no issues found in 408 source files ``` ## Real Behavior Proof - Environment: macOS (Darwin 24.6.0), Python 3.14.5, headroom-ai dev checkout built via `uv sync --extra dev` + `maturin develop -m crates/headroom-py/Cargo.toml` (real `headroom._core` build, not mocked), `tree-sitter==0.25.2` / `tree-sitter-language-pack` per the pinned `[code]` extra. - Exact command / steps: ran `CodeAwareCompressor(CodeCompressorConfig(min_tokens_for_compression=10, enable_ccr=False)).compress(open("sdk/typescript/src/client.ts").read(), language="typescript")` identically against `git stash`-ed (pre-fix) and current (post-fix) trees; full snippet and additional samples below. - Observed result: `client.ts` (real 20KB SDK file in this repo) went from `compression_ratio=1.0` with a silent fallback (`export export class HeadroomClient` in the raw AST attempt, invalid syntax) to `compression_ratio=0.942`, `syntax_valid=True` — real compression, no duplication; full before/after table below. - Not tested: real-world repos beyond this repo's own SDK sample and the bundled benchmark fixture — broader corpus testing may follow as a comment on this PR. **Exact command, full snippet:** ```python from headroom.transforms.code_compressor import CodeAwareCompressor, CodeCompressorConfig compressor = CodeAwareCompressor(CodeCompressorConfig(min_tokens_for_compression=10, enable_ccr=False)) with open("sdk/typescript/src/client.ts") as f: code = f.read() result = compressor.compress(code, language="typescript") ``` **Observed result, before vs. after, real code:** | Sample | Before (main) | After (this fix) | |---|---|---| | `sdk/typescript/src/client.ts` (real 20KB SDK file, this repo) | `compression_ratio=1.0`, silent fallback — `export export class HeadroomClient` in the raw AST attempt, invalid syntax | `compression_ratio=0.942`, `syntax_valid=True` — real compression, no duplication | | TS fixture exercising both bugs (exported fn/class + doc comments) | `compression_ratio=1.0`, silent fallback | `compression_ratio=0.993`, `syntax_valid=True` | | `middleware/ratelimit.go` (bundled benchmark sample) | `compression_ratio=0.862`, `syntax_valid=True` — unaffected (Go bug already fixed on `main` by #1668) | `compression_ratio=0.862`, `syntax_valid=True` — unchanged, confirms no regression | | `generate_go_code(3)` (existing test fixture) | `compression_ratio=0.498` | `compression_ratio=0.498` — unchanged, confirms no regression | On code shaped to actually exercise elision (function bodies long enough to exceed `max_body_lines=5`), TypeScript compresses in line with other languages once the correctness bug stops blocking it entirely: | Language | Compression savings (synthetic fixture, ~10-line function bodies) | |---|---| | Python | 64.4% | | Go | 52.3% | | TypeScript | 49.0% | | JavaScript | 42.8% | (`client.ts`'s real-world 5.8% savings is lower than the synthetic TypeScript number above because most of its methods are ≤5 lines — under the elision threshold regardless of language — not because of a language-specific limitation.) ## 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 - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes The Go brace-duplication bug that motivated this investigation was already fixed on `main` (#1668, merged before this branch was based) — confirmed via the minimal repro and `ratelimit.go`, both compress cleanly with no duplicated braces. This PR fixes what was still actually broken: the TS/JS `export`-duplication bug and the doc-comment displacement bug (both present across languages), found empirically while verifying the original bug report against the current `main`. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-10 01:51:32 +08:00
assert result.compression_ratio < 1.0
assert result.syntax_valid is True
assert result.language == CodeLanguage.GO
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}",
),
feat(transforms): first-class C# support in CodeAwareCompressor (#1926) Refs #1664 ## Description First-class C# support in `CodeAwareCompressor` via the tree-sitter `csharp` grammar, at parity with Java/C++/Rust: `using` directives, namespace headers, and type/member signatures preserved verbatim; method/constructor/destructor/operator/local-function bodies compressed; malformed input passes through unchanged. **No new dependencies** — the grammar ships inside the already-pinned `tree-sitter-language-pack==0.13.0` (resolves only as `"csharp"`; `c_sharp`/`cs` raise `LookupError`). Spec and maintainer go-ahead in the issue. Closes #1664 ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] 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 - `CodeLanguage.CSHARP` + full `_LANG_CONFIGS` entry; `_LANGUAGE_PREFILTER` and `content_detector` patterns chosen to be C#-distinctive (so Java doesn't mis-tag). - New data-driven `LangConfig` fields (pattern of #1334's `class_body_node_types`): `container_node_types` — block-scoped `namespace { }` routed through class compression so members compress without the wrapper being re-emitted verbatim; `opaque_node_types` — `#if`…`#endif` wrappers preserved verbatim without recursion (recursing + wrapper re-emit duplicated whole files, up to ~1.9x input on real repos); `#if` blocks wrapping only usings are emitted with the imports so they stay ahead of type declarations. - Shared-path fixes surfaced by real C# repos, each guarded and covered by a fail-before test: keep an Allman `{` on its own line in class reconstruction (K&R path byte-for-byte unchanged; Allman Java now compresses instead of falling back); line-based child extraction no longer swallows the following line for nodes ending at column 0 (C# `#region`/`#endregion` span their trailing newline — the over-slice duplicated the next member's signature or the closing brace); uncaptured top-level nodes preceding the first captured node (license banners, `#region License`) are emitted first instead of relocated below the code (tree-sitter-c-sharp rejects top-level `#region` after a type declaration, so relocation forfeited compression for the whole file). - `TestCSharpSupport` (8 tests) + a C# case in the parametrized member-container test; CHANGELOG entry. ## 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 $ python -m pytest tests/test_transforms/test_code_compressor.py -q 2 failed, 78 passed, 1 warning, 4 errors # the 2 failures / 4 errors reproduce # identically on main in the same env # (network-dependent tokenizer setup) Fail-before: with both changed sources reverted to main, the new C#-scoped selection reports "10 failed, 5 passed" (the 5 other languages keep passing); on the branch: "15 passed". $ ruff check headroom/transforms/code_compressor.py headroom/transforms/content_detector.py tests/test_transforms/test_code_compressor.py All checks passed! $ ruff format --check <same files> 3 files already formatted ``` ## Real Behavior Proof - Environment: Linux 6.8.0 aarch64, Python 3.10.12; `uv run --no-project --with "tree-sitter-language-pack==0.13.0" --with "tree-sitter>=0.25.2,<0.26" --with "pydantic>=2.0.0"`; real `CodeAwareCompressor` (`CodeCompressorConfig(enable_ccr=False)`, otherwise defaults), no mocks. - Exact command / steps: cloned two real .NET repos at depth 1 (`github.com/JamesNK/Newtonsoft.Json @ 4f73e74`, `github.com/App-vNext/Polly @ 7a1d10f`), ran `python proof_csharp.py <repo>` over every `.cs` file (chars/4 token estimate; tiktoken BPE download unavailable in my sandbox). Script in the collapsed section below. - Observed result: 16.1% tokens saved on Newtonsoft.Json (945/945 syntax-valid), 37.8% on Polly (797/797 syntax-valid), zero content duplication; full output: ```text repo: Newtonsoft.Json (945 .cs files) tokens before: 1,777,691 after: 1,490,629 saved: 287,062 (16.1%) files compressed: 479 pass-through: 466 inflated(>before): 19 syntax_valid: 945/945 latency ms P50: 0.7 P95: 18.7 P99: 44.1 max: 255.0 mean: 3.5 repo: Polly (797 .cs files) tokens before: 1,100,523 after: 684,303 saved: 416,220 (37.8%) files compressed: 693 pass-through: 104 inflated(>before): 15 syntax_valid: 797/797 latency ms P50: 0.8 P95: 11.6 P99: 28.9 max: 74.1 mean: 2.4 ``` After rebasing onto current `main` (which touched the same transform files via #1906/#1747/#1668) I re-ran the Polly proof on the rebased tree: 37.8% saved, 797/797 syntax-valid, P99 28.5ms — unchanged. Signatures/properties verbatim, bodies elided with call summaries, `using` order and preproc balance intact; residual "inflated" files are +2…+209 chars of assembly blank lines, not duplicated content. Newtonsoft is the adversarial case (multi-targeting: heavy `#if`, `#region`, Allman) — its conditional regions stay verbatim by design. Latency at parity with Java (<50ms P99; max is the pre-existing symbol-analysis cost on ~1800+-line files, shared with other languages). - Not tested: proxy end-to-end path with C# through `ContentRouter` (tested the `CodeAwareCompressor` API directly); CCR retrieval round-trips (`enable_ccr=False` in proof runs); exact tiktoken counts (chars/4 estimate — relative ratios are tokenizer-independent); Windows/macOS; full native `uv run pytest` with the Rust extension (ran the complete `test_code_compressor.py` in a lightweight venv; its 2 failures/4 errors reproduce identically on `main`); `mypy`. <details> <summary>proof_csharp.py (reproducible)</summary> ```python """Real behavior proof: run the real CodeAwareCompressor over a .NET repo.""" import pathlib import statistics import sys import time from headroom.transforms.code_compressor import ( CodeAwareCompressor, CodeCompressorConfig, ) try: import tiktoken ENC = tiktoken.get_encoding("cl100k_base") def toks(s: str) -> int: return len(ENC.encode(s, disallowed_special=())) except Exception: def toks(s: str) -> int: return len(s) // 4 target = pathlib.Path(sys.argv[1]) comp = CodeAwareCompressor(CodeCompressorConfig(enable_ccr=False)) tot_before = tot_after = 0 n_files = n_compressed = n_valid = n_passthrough = n_inflated = 0 times_ms: list[float] = [] for f in sorted(target.rglob("*.cs")): try: code = f.read_text(encoding="utf-8-sig", errors="replace") except OSError: continue t0 = time.perf_counter() r = comp.compress(code, language="csharp") times_ms.append((time.perf_counter() - t0) * 1000) n_files += 1 b, a = toks(code), toks(r.compressed) tot_before += b tot_after += a if r.compressed == code: n_passthrough += 1 else: n_compressed += 1 if r.syntax_valid: n_valid += 1 if a > b: n_inflated += 1 times_ms.sort() p = lambda q: times_ms[min(int(len(times_ms) * q), len(times_ms) - 1)] print(f"repo: {target.name} ({n_files} .cs files)") print(f" tokens before: {tot_before:,} after: {tot_after:,} saved: {tot_before - tot_after:,} ({(1 - tot_after / tot_before) * 100:.1f}%)") print(f" files compressed: {n_compressed} pass-through: {n_passthrough} inflated(>before): {n_inflated}") print(f" syntax_valid: {n_valid}/{n_files}") print(f" latency ms P50: {p(0.50):.1f} P95: {p(0.95):.1f} P99: {p(0.99):.1f} max: {times_ms[-1]:.1f} mean: {statistics.mean(times_ms):.1f}") ``` </details> ## 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 - [x] 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 — terminal evidence above. ## Additional Notes - Dependency justification: none added, none bumped; the `csharp` grammar is inside the already-pinned `tree-sitter-language-pack==0.13.0` wheel; `uv.lock` untouched. - Architecture: malformed input passes through byte-identical; every risky construct prefers the false negative (verbatim) over corruption; invalid reassembly falls back to the original via the existing validation gate (observed live); no new imports at module load; P99 <50ms on both proof repos. - Known v1 limitations (deliberate false negatives, possible follow-ups): expression-bodied members and property accessor bodies stay verbatim; declarations inside `#if` regions stay verbatim. - Related pre-existing finding, out of scope: C/C++ exhibit the same `#if`-wrapper duplication on `main` (an `#if`-wrapped C++ class is emitted twice, ratio 1.62). Happy to file separately. - `mypy` unchecked above because I did not run it in my environment.
2026-07-12 19:54:38 +02:00
(
"csharp",
(
"namespace Acme\n"
"{\n"
" public class Calc\n"
" {\n"
" public int Compute(int x)\n"
" {\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"
"}\n"
),
"public int Compute(int x)",
5,
"return e;",
"}\n}",
),
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
],
)
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)
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}")
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
# =============================================================================
# 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,
)
)
fix(code-compressor): recover valid Python rewrites after local syntax rejection (#2202) ## Description A compile-invalid Python definition rewrite currently makes `CodeAwareCompressor` discard every otherwise valid rewrite in the file and return the original source at 0 percent reduction. The existing whole-file safety guard stays in place, while a Python-only recovery replay now preserves the rejected definition and keeps independent valid compression. The recovery reuses the current Python validation authority in `ast.parse()` plus `compile()`, runs only after the first assembled module already fails `_verify_syntax()`, and stays out of non-Python paths. Closes #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 - Added a Python-only recovery replay after the first assembled module fails syntax validation. - Preserved only the invalid function or class rewrite while allowing independent valid definitions to remain compressed. - Kept the existing whole-file syntax guard and original-source fallback as the terminal safety check. - Added focused invalid-node, valid-modern-syntax, and fail-safe coverage. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_invalid_node_falls_back_locally tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_does_not_block_valid_modern_syntax tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_still_returns_original_when_all_candidates_invalid tests/test_transforms/test_code_compressor.py::TestEdgeCases::test_syntax_errors_in_input tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_ast_preserves_structure_for_python -v`) - [x] Linting passes (`uv run ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_invalid_node_falls_back_locally tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_does_not_block_valid_modern_syntax tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_still_returns_original_when_all_candidates_invalid tests/test_transforms/test_code_compressor.py::TestEdgeCases::test_syntax_errors_in_input tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_ast_preserves_structure_for_python -v 5 passed in 0.34s uv run ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py All checks passed! uv run ruff format headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py --check 2 files already formatted ``` ## Real Behavior Proof - Environment: Windows, synced `uv` environment with `dev` and `code` extras installed - Exact command / steps: run the focused invalid-node regression through public `compress(..., language="python")` - Observed result: `1 passed in 0.19s`; the invalid candidate stays original, the neighboring valid candidate remains compressed, and `headroom-PR-TARGET-1233-PROOF.md` records the base `ratio=1.0` whole-file rollback against the fixed head behavior. - Not tested: the stale future-import mismatch discussed in the old issue comment, already covered on current main ## 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 - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - The stale future-import comment on #1233 is not the live slice here; current main already validates Python with `compile()` and already covers that ordering case. - This fix keeps the existing whole-file fail-safe and does not broaden into cross-language recovery or new syntax models. - `CHANGELOG.md` remains unchanged because Headroom generates release notes from conventional commits. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 23:19:46 -04:00
def _recovery_compressor(self, **overrides):
defaults = {
"min_tokens_for_compression": 1,
"max_body_lines": 2,
"enable_ccr": False,
"semantic_analysis": False,
}
defaults.update(overrides)
return CodeAwareCompressor(CodeCompressorConfig(**defaults))
def _python_recovery_fixture(self) -> str:
return textwrap.dedent(
"""\
from pathlib import Path
def expand_search_roots(user_root: str) -> list[Path]:
root = Path(user_root)
candidates = [root]
for child in root.iterdir():
candidates.append(child.resolve())
return candidates
def load_user_overrides(config_path: str) -> dict[str, str]:
config: dict[str, str] = {}
for line in Path(config_path).read_text().splitlines():
if "=" in line:
key, value = line.split("=", 1)
config[key.strip()] = value.strip()
return config
"""
)
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_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-compressor): recover valid Python rewrites after local syntax rejection (#2202) ## Description A compile-invalid Python definition rewrite currently makes `CodeAwareCompressor` discard every otherwise valid rewrite in the file and return the original source at 0 percent reduction. The existing whole-file safety guard stays in place, while a Python-only recovery replay now preserves the rejected definition and keeps independent valid compression. The recovery reuses the current Python validation authority in `ast.parse()` plus `compile()`, runs only after the first assembled module already fails `_verify_syntax()`, and stays out of non-Python paths. Closes #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 - Added a Python-only recovery replay after the first assembled module fails syntax validation. - Preserved only the invalid function or class rewrite while allowing independent valid definitions to remain compressed. - Kept the existing whole-file syntax guard and original-source fallback as the terminal safety check. - Added focused invalid-node, valid-modern-syntax, and fail-safe coverage. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_invalid_node_falls_back_locally tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_does_not_block_valid_modern_syntax tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_still_returns_original_when_all_candidates_invalid tests/test_transforms/test_code_compressor.py::TestEdgeCases::test_syntax_errors_in_input tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_ast_preserves_structure_for_python -v`) - [x] Linting passes (`uv run ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_invalid_node_falls_back_locally tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_does_not_block_valid_modern_syntax tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_still_returns_original_when_all_candidates_invalid tests/test_transforms/test_code_compressor.py::TestEdgeCases::test_syntax_errors_in_input tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_ast_preserves_structure_for_python -v 5 passed in 0.34s uv run ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py All checks passed! uv run ruff format headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py --check 2 files already formatted ``` ## Real Behavior Proof - Environment: Windows, synced `uv` environment with `dev` and `code` extras installed - Exact command / steps: run the focused invalid-node regression through public `compress(..., language="python")` - Observed result: `1 passed in 0.19s`; the invalid candidate stays original, the neighboring valid candidate remains compressed, and `headroom-PR-TARGET-1233-PROOF.md` records the base `ratio=1.0` whole-file rollback against the fixed head behavior. - Not tested: the stale future-import mismatch discussed in the old issue comment, already covered on current main ## 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 - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - The stale future-import comment on #1233 is not the live slice here; current main already validates Python with `compile()` and already covers that ordering case. - This fix keeps the existing whole-file fail-safe and does not broaden into cross-language recovery or new syntax models. - `CHANGELOG.md` remains unchanged because Headroom generates release notes from conventional commits. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 23:19:46 -04:00
def test_python_invalid_node_falls_back_locally(self):
"""One invalid Python rewrite must preserve only that definition."""
compressor = self._recovery_compressor()
code = self._python_recovery_fixture()
original_compress_function_ast = compressor._compress_function_ast
def _patched(node, code_text, language, lang_config, body_limits, analysis):
name = cc._get_definition_name(node)
if name == "expand_search_roots":
return "def expand_search_roots(user_root: str) -> list[Path]:\n if True\n"
if name == "load_user_overrides":
return (
"def load_user_overrides(config_path: str) -> dict[str, str]:\n"
" config: dict[str, str] = {}\n"
" # [4 lines omitted]\n"
" return config"
)
return original_compress_function_ast(
node,
code_text,
language,
lang_config,
body_limits,
analysis,
)
with patch.object(compressor, "_compress_function_ast", side_effect=_patched):
result = compressor.compress(code, language="python")
assert result.syntax_valid is True
assert result.compression_ratio < 1.0
assert result.compressed != code
assert "if True" not in result.compressed
assert "for child in root.iterdir():" in result.compressed
assert "# [4 lines omitted]" in result.compressed
compile(result.compressed, "<test>", "exec")
def test_python_recovery_does_not_block_valid_modern_syntax(self):
"""Valid decorators, nested defs, and match syntax still compress."""
compressor = self._recovery_compressor(max_body_lines=4)
code = textwrap.dedent(
"""\
from dataclasses import dataclass
def traced(fn):
return fn
@dataclass
class Command:
name: str
payload: dict[str, int]
@traced
def route_command(command: Command) -> str:
def normalize(value: str) -> str:
return value.strip().lower()
match normalize(command.name):
case "ping":
return "pong"
case "echo":
return str(command.payload)
case _:
return "unknown"
"""
)
result = compressor.compress(code, language="python")
assert result.syntax_valid is True
assert result.compression_ratio < 1.0
assert "@traced" in result.compressed
assert "class Command:" in result.compressed
assert "def route_command(command: Command) -> str:" in result.compressed
compile(result.compressed, "<test>", "exec")
def test_python_recovery_still_returns_original_when_all_candidates_invalid(self):
"""Recovery keeps the terminal whole-file fail-safe."""
compressor = self._recovery_compressor()
code = self._python_recovery_fixture()
def _patched(node, _code_text, _language, _lang_config, _body_limits, _analysis):
name = cc._get_definition_name(node) or "broken"
return f"def {name}(:\n pass"
with patch.object(compressor, "_compress_function_ast", side_effect=_patched):
result = compressor.compress(code, language="python")
assert result.syntax_valid is True
assert result.compression_ratio == 1.0
assert "(:\n" not in result.compressed
assert "for child in root.iterdir():" in result.compressed
assert 'key, value = line.split("=", 1)' in result.compressed
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
feat(transforms): first-class C# support in CodeAwareCompressor (#1926) Refs #1664 ## Description First-class C# support in `CodeAwareCompressor` via the tree-sitter `csharp` grammar, at parity with Java/C++/Rust: `using` directives, namespace headers, and type/member signatures preserved verbatim; method/constructor/destructor/operator/local-function bodies compressed; malformed input passes through unchanged. **No new dependencies** — the grammar ships inside the already-pinned `tree-sitter-language-pack==0.13.0` (resolves only as `"csharp"`; `c_sharp`/`cs` raise `LookupError`). Spec and maintainer go-ahead in the issue. Closes #1664 ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] 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 - `CodeLanguage.CSHARP` + full `_LANG_CONFIGS` entry; `_LANGUAGE_PREFILTER` and `content_detector` patterns chosen to be C#-distinctive (so Java doesn't mis-tag). - New data-driven `LangConfig` fields (pattern of #1334's `class_body_node_types`): `container_node_types` — block-scoped `namespace { }` routed through class compression so members compress without the wrapper being re-emitted verbatim; `opaque_node_types` — `#if`…`#endif` wrappers preserved verbatim without recursion (recursing + wrapper re-emit duplicated whole files, up to ~1.9x input on real repos); `#if` blocks wrapping only usings are emitted with the imports so they stay ahead of type declarations. - Shared-path fixes surfaced by real C# repos, each guarded and covered by a fail-before test: keep an Allman `{` on its own line in class reconstruction (K&R path byte-for-byte unchanged; Allman Java now compresses instead of falling back); line-based child extraction no longer swallows the following line for nodes ending at column 0 (C# `#region`/`#endregion` span their trailing newline — the over-slice duplicated the next member's signature or the closing brace); uncaptured top-level nodes preceding the first captured node (license banners, `#region License`) are emitted first instead of relocated below the code (tree-sitter-c-sharp rejects top-level `#region` after a type declaration, so relocation forfeited compression for the whole file). - `TestCSharpSupport` (8 tests) + a C# case in the parametrized member-container test; CHANGELOG entry. ## 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 $ python -m pytest tests/test_transforms/test_code_compressor.py -q 2 failed, 78 passed, 1 warning, 4 errors # the 2 failures / 4 errors reproduce # identically on main in the same env # (network-dependent tokenizer setup) Fail-before: with both changed sources reverted to main, the new C#-scoped selection reports "10 failed, 5 passed" (the 5 other languages keep passing); on the branch: "15 passed". $ ruff check headroom/transforms/code_compressor.py headroom/transforms/content_detector.py tests/test_transforms/test_code_compressor.py All checks passed! $ ruff format --check <same files> 3 files already formatted ``` ## Real Behavior Proof - Environment: Linux 6.8.0 aarch64, Python 3.10.12; `uv run --no-project --with "tree-sitter-language-pack==0.13.0" --with "tree-sitter>=0.25.2,<0.26" --with "pydantic>=2.0.0"`; real `CodeAwareCompressor` (`CodeCompressorConfig(enable_ccr=False)`, otherwise defaults), no mocks. - Exact command / steps: cloned two real .NET repos at depth 1 (`github.com/JamesNK/Newtonsoft.Json @ 4f73e74`, `github.com/App-vNext/Polly @ 7a1d10f`), ran `python proof_csharp.py <repo>` over every `.cs` file (chars/4 token estimate; tiktoken BPE download unavailable in my sandbox). Script in the collapsed section below. - Observed result: 16.1% tokens saved on Newtonsoft.Json (945/945 syntax-valid), 37.8% on Polly (797/797 syntax-valid), zero content duplication; full output: ```text repo: Newtonsoft.Json (945 .cs files) tokens before: 1,777,691 after: 1,490,629 saved: 287,062 (16.1%) files compressed: 479 pass-through: 466 inflated(>before): 19 syntax_valid: 945/945 latency ms P50: 0.7 P95: 18.7 P99: 44.1 max: 255.0 mean: 3.5 repo: Polly (797 .cs files) tokens before: 1,100,523 after: 684,303 saved: 416,220 (37.8%) files compressed: 693 pass-through: 104 inflated(>before): 15 syntax_valid: 797/797 latency ms P50: 0.8 P95: 11.6 P99: 28.9 max: 74.1 mean: 2.4 ``` After rebasing onto current `main` (which touched the same transform files via #1906/#1747/#1668) I re-ran the Polly proof on the rebased tree: 37.8% saved, 797/797 syntax-valid, P99 28.5ms — unchanged. Signatures/properties verbatim, bodies elided with call summaries, `using` order and preproc balance intact; residual "inflated" files are +2…+209 chars of assembly blank lines, not duplicated content. Newtonsoft is the adversarial case (multi-targeting: heavy `#if`, `#region`, Allman) — its conditional regions stay verbatim by design. Latency at parity with Java (<50ms P99; max is the pre-existing symbol-analysis cost on ~1800+-line files, shared with other languages). - Not tested: proxy end-to-end path with C# through `ContentRouter` (tested the `CodeAwareCompressor` API directly); CCR retrieval round-trips (`enable_ccr=False` in proof runs); exact tiktoken counts (chars/4 estimate — relative ratios are tokenizer-independent); Windows/macOS; full native `uv run pytest` with the Rust extension (ran the complete `test_code_compressor.py` in a lightweight venv; its 2 failures/4 errors reproduce identically on `main`); `mypy`. <details> <summary>proof_csharp.py (reproducible)</summary> ```python """Real behavior proof: run the real CodeAwareCompressor over a .NET repo.""" import pathlib import statistics import sys import time from headroom.transforms.code_compressor import ( CodeAwareCompressor, CodeCompressorConfig, ) try: import tiktoken ENC = tiktoken.get_encoding("cl100k_base") def toks(s: str) -> int: return len(ENC.encode(s, disallowed_special=())) except Exception: def toks(s: str) -> int: return len(s) // 4 target = pathlib.Path(sys.argv[1]) comp = CodeAwareCompressor(CodeCompressorConfig(enable_ccr=False)) tot_before = tot_after = 0 n_files = n_compressed = n_valid = n_passthrough = n_inflated = 0 times_ms: list[float] = [] for f in sorted(target.rglob("*.cs")): try: code = f.read_text(encoding="utf-8-sig", errors="replace") except OSError: continue t0 = time.perf_counter() r = comp.compress(code, language="csharp") times_ms.append((time.perf_counter() - t0) * 1000) n_files += 1 b, a = toks(code), toks(r.compressed) tot_before += b tot_after += a if r.compressed == code: n_passthrough += 1 else: n_compressed += 1 if r.syntax_valid: n_valid += 1 if a > b: n_inflated += 1 times_ms.sort() p = lambda q: times_ms[min(int(len(times_ms) * q), len(times_ms) - 1)] print(f"repo: {target.name} ({n_files} .cs files)") print(f" tokens before: {tot_before:,} after: {tot_after:,} saved: {tot_before - tot_after:,} ({(1 - tot_after / tot_before) * 100:.1f}%)") print(f" files compressed: {n_compressed} pass-through: {n_passthrough} inflated(>before): {n_inflated}") print(f" syntax_valid: {n_valid}/{n_files}") print(f" latency ms P50: {p(0.50):.1f} P95: {p(0.95):.1f} P99: {p(0.99):.1f} max: {times_ms[-1]:.1f} mean: {statistics.mean(times_ms):.1f}") ``` </details> ## 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 - [x] 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 — terminal evidence above. ## Additional Notes - Dependency justification: none added, none bumped; the `csharp` grammar is inside the already-pinned `tree-sitter-language-pack==0.13.0` wheel; `uv.lock` untouched. - Architecture: malformed input passes through byte-identical; every risky construct prefers the false negative (verbatim) over corruption; invalid reassembly falls back to the original via the existing validation gate (observed live); no new imports at module load; P99 <50ms on both proof repos. - Known v1 limitations (deliberate false negatives, possible follow-ups): expression-bodied members and property accessor bodies stay verbatim; declarations inside `#if` regions stay verbatim. - Related pre-existing finding, out of scope: C/C++ exhibit the same `#if`-wrapper duplication on `main` (an `#if`-wrapped C++ class is emitted twice, ratio 1.62). Happy to file separately. - `mypy` unchecked above because I did not run it in my environment.
2026-07-12 19:54:38 +02:00
@pytest.mark.skipif(not TREE_SITTER_INSTALLED, reason="tree-sitter grammar pack not installed")
class TestCSharpSupport:
"""C# (``csharp`` grammar) parity with Java/C++/Rust: signatures preserved
verbatim, method/constructor bodies compressed, block-scoped namespaces
routed through container compression (no verbatim re-dump), file-scoped
namespace headers kept ahead of types, malformed input passed through.
"""
def _compressor(self):
return CodeAwareCompressor(
CodeCompressorConfig(
min_tokens_for_compression=1,
max_body_lines=1,
enable_ccr=False,
)
)
def test_block_scoped_namespace_compresses_without_redumping(self):
"""Block-scoped ``namespace { }`` wraps types in a declaration_list; the
container must compress its members and NOT be re-emitted verbatim
(which would duplicate the class and defeat compression the C#
analogue of the #1318 class-member trap, one nesting level deeper)."""
code = (
"using System;\n"
"\n"
"namespace Acme.Widgets\n"
"{\n"
" public class WidgetService\n"
" {\n"
" public int Process(int x)\n"
" {\n"
" var a = x + 1;\n"
" var b = a + 2;\n"
" var c = b + 3;\n"
" Console.WriteLine(c);\n"
" return c;\n"
" }\n"
" }\n"
"}\n"
)
result = self._compressor().compress(code, language="csharp")
assert result.language == CodeLanguage.CSHARP
assert result.syntax_valid is True
assert result.compression_ratio < 1.0
# namespace + type headers preserved verbatim
assert "namespace Acme.Widgets" in result.compressed
assert "public class WidgetService" in result.compressed
assert "public int Process(int x)" in result.compressed
# method body actually compressed
assert "lines omitted" in result.compressed
assert "return c;" not in result.compressed
# the class is emitted exactly once (no re-dump / duplication)
assert result.compressed.count("class WidgetService") == 1
# block-namespace wrapper still closes
assert result.compressed.rstrip().endswith("}")
def test_file_scoped_namespace_directive_precedes_types(self):
"""A file-scoped ``namespace X;`` must stay ahead of the type
declarations; emitting it after a type would not be valid C#."""
code = (
"using System;\n"
"\n"
"namespace Acme.Tools;\n"
"\n"
"public class Helper\n"
"{\n"
" public int Add(int a, int b)\n"
" {\n"
" var s = a + b;\n"
" var t = s + 1;\n"
" var u = t + 2;\n"
" return u;\n"
" }\n"
"}\n"
)
result = self._compressor().compress(code, language="csharp")
assert result.language == CodeLanguage.CSHARP
assert result.syntax_valid is True
assert result.compression_ratio < 1.0
assert "namespace Acme.Tools;" in result.compressed
assert "public class Helper" in result.compressed
assert result.compressed.index("namespace Acme.Tools;") < result.compressed.index(
"class Helper"
)
def test_record_preserves_positional_parameters(self):
"""Record primary-constructor parameters are part of the signature and
must be preserved verbatim while the method body compresses."""
code = (
"namespace Acme.Models;\n"
"\n"
"public record Point(int X, int Y)\n"
"{\n"
" public double Dist()\n"
" {\n"
" var sq = X * X + Y * Y;\n"
" var r = System.Math.Sqrt(sq);\n"
" System.Console.WriteLine(r);\n"
" return r;\n"
" }\n"
"}\n"
)
result = self._compressor().compress(code, language="csharp")
assert result.language == CodeLanguage.CSHARP
assert result.syntax_valid is True
assert result.compression_ratio < 1.0
assert "public record Point(int X, int Y)" in result.compressed
assert "public double Dist()" in result.compressed
assert "lines omitted" in result.compressed
def test_preprocessor_wrapped_file_is_not_duplicated(self):
"""A file wrapped in ``#if``/``#endif`` (idiomatic in multi-targeted
.NET code) must not have its content emitted twice. The visitor
recurses into ``preproc_if`` and captures the declarations, but the
top-level pass used to re-emit the uncaptured wrapper verbatim,
duplicating the whole file (observed on real repos: output up to
~1.9x the input). Opaque handling keeps it verbatim exactly once."""
code = (
"#if !HAVE_TRACE_WRITER\n"
"using System;\n"
"\n"
"namespace Acme\n"
"{\n"
" public enum TraceLevel\n"
" {\n"
" Off = 0,\n"
" Error = 1,\n"
" }\n"
"}\n"
"#endif\n"
)
result = self._compressor().compress(code, language="csharp")
assert result.syntax_valid is True
# Content appears exactly once — no wrapper re-dump.
assert result.compressed.count("enum TraceLevel") == 1
assert result.compressed.count("using System;") == 1
# Never larger than the input (verbatim pass-through is acceptable).
assert result.compression_ratio <= 1.0
# Conditional directives stay balanced.
assert result.compressed.count("#if") == result.compressed.count("#endif")
def test_preprocessor_wrapped_usings_stay_ahead_of_types(self):
"""``#if``-wrapped using directives (idiomatic for multi-targeting)
must be emitted with the imports. Appending them as trailing top-level
code puts usings after type declarations invalid C#, which trips the
output syntax check and falls back to no compression at all (observed
on real repos: hundreds of files silently uncompressed)."""
code = (
"using System;\n"
"#if HAVE_BIG_INTEGER\n"
"using System.Numerics;\n"
"#endif\n"
"\n"
"namespace Acme\n"
"{\n"
" public class Calc\n"
" {\n"
" public int Compute(int x)\n"
" {\n"
" var a = x + 1;\n"
" var b = a * 2;\n"
" var c = b - 3;\n"
" var d = c / 4;\n"
" return d;\n"
" }\n"
" }\n"
"}\n"
)
result = self._compressor().compress(code, language="csharp")
assert result.syntax_valid is True
# Compression must actually happen (no silent fallback).
assert result.compression_ratio < 1.0
assert "lines omitted" in result.compressed
# The conditional import block survives verbatim, ahead of the types.
assert "#if HAVE_BIG_INTEGER" in result.compressed
assert "using System.Numerics;" in result.compressed
assert result.compressed.index("#endif") < result.compressed.index("class Calc")
def test_region_markers_inside_class_compress_cleanly(self):
"""``#region``/``#endregion`` markers span their trailing newline, so
their end_point sits at column 0 of the NEXT line. Line-based child
extraction used to swallow that line duplicating the following
method's signature and the class's closing brace (invalid output
silent fallback to no compression on real repos)."""
code = (
"namespace Acme\n"
"{\n"
" public class Svc\n"
" {\n"
" #region Api\n"
" public int F(int x)\n"
" {\n"
" var a = x + 1;\n"
" var b = a * 2;\n"
" var c = b - 3;\n"
" return c;\n"
" }\n"
" #endregion\n"
" }\n"
"}\n"
)
result = self._compressor().compress(code, language="csharp")
assert result.syntax_valid is True
assert result.compression_ratio < 1.0
assert "lines omitted" in result.compressed
# Signature emitted exactly once (regions used to duplicate it).
assert result.compressed.count("public int F(int x)") == 1
# Region markers survive, braces stay balanced.
assert "#region Api" in result.compressed
assert "#endregion" in result.compressed
assert result.compressed.count("{") == result.compressed.count("}")
def test_license_region_header_stays_on_top(self):
"""A ``#region License`` banner above the usings (near-universal in
real .NET code) must stay at the top of the file. Relocating it after
the type declarations the old behavior for uncaptured top-level
nodes is rejected by tree-sitter-c-sharp, failing output validation
and silently forfeiting compression for the whole file."""
code = (
"#region License\n"
"// Copyright (c) 2007 Example Corp.\n"
"// Licensed under the MIT license.\n"
"#endregion\n"
"\n"
"using System;\n"
"\n"
"namespace Acme\n"
"{\n"
" public class Svc\n"
" {\n"
" public int F(int x)\n"
" {\n"
" var a = x + 1;\n"
" var b = a * 2;\n"
" var c = b - 3;\n"
" return c;\n"
" }\n"
" }\n"
"}\n"
)
result = self._compressor().compress(code, language="csharp")
assert result.syntax_valid is True
assert result.compression_ratio < 1.0
assert "lines omitted" in result.compressed
# Header block survives, in order, ahead of everything else.
assert result.compressed.index("#region License") < result.compressed.index("using System;")
assert result.compressed.index("// Copyright") < result.compressed.index("#endregion")
assert result.compressed.index("#endregion") < result.compressed.index("class Svc")
def test_malformed_csharp_passes_through_unchanged(self):
"""Malformed C# must pass through unchanged (no data loss; prefer false
negatives over serving broken or altered code)."""
code = (
"namespace Broken\n"
"{\n"
" public class Oops\n"
" {\n"
" public int F(int x)\n"
" {\n"
" var a = x + 1\n" # missing ';' and unclosed braces
" return a\n"
)
result = self._compressor().compress(code, language="csharp")
assert result.compressed == code
assert result.compression_ratio == 1.0
def test_detect_language_identifies_csharp(self):
"""Auto-detection (no explicit language hint) recognizes C#."""
code = (
"using System;\n"
"\n"
"namespace Acme\n"
"{\n"
" public class Svc\n"
" {\n"
" public int Total { get; set; }\n"
" public int Add(int a, int b)\n"
" {\n"
" return a + b;\n"
" }\n"
" }\n"
"}\n"
)
lang, confidence = detect_language(code)
assert lang == CodeLanguage.CSHARP
assert confidence > 0.0
feat(code): add PHP support to CodeAwareCompressor (#2423) ## Description Adds PHP to `CodeAwareCompressor`, fixing #201. PHP was already *detected* as code (Magika labels in `headroom/compression/detector.py` include `php`, and the Rust `magika_detector.rs` lists it too) but there was no PHP `LangConfig`, so PHP content silently passed through uncompressed. This wires PHP through the tree-sitter compression path following the C# pattern (the most recently added, fully functional language — deliberately not the quarantined Perl path). A secondary detection bug is fixed along the way: PHP's `$variables` match Perl's prefilter regex, and the existing Perl-dominance guard in `detect_language` returned `UNKNOWN` for PHP files. An explicit `<?php` open tag — which no Perl source contains — now drops Perl from the candidate set before that guard runs. ## Type of Change - [ ] Bug fix - [x] New feature - [ ] Documentation update - [ ] Refactor - [ ] Other ## Changes Made - `headroom/transforms/code_compressor.py`: `CodeLanguage.PHP` + `phtml`/`php5`/`php7`/`php8` aliases; PHP `LangConfig` built from the actual tree-sitter-php grammar (node names verified by parsing samples): `namespace_use_declaration` imports, `function_definition`/`method_declaration` functions, `class_declaration`/`interface_declaration`/`trait_declaration` classes, `enum_declaration` types, `declaration_list` class bodies, `compound_statement` function bodies. `namespace_definition` maps to `package_node` so statement-scoped `namespace App;` hoists ahead of the `use` imports (required PHP ordering); the rare block-scoped `namespace A { }` form takes the same path and is preserved verbatim — valid output, just no compression inside the block. PHP prefilter regexes added; supported-languages error message updated; `<?php`-tag Perl disambiguation in `detect_language`. - `headroom/transforms/content_detector.py`: `php` entry in `_CODE_PATTERNS` so raw PHP classifies as `SOURCE_CODE` and reaches the code-aware route. - `tests/test_transforms/test_code_compressor.py`: new `TestPhpSupport` mirroring `TestCSharpSupport` — signatures preserved / bodies elided, `<?php` → `namespace` → `use` → declarations ordering, auto-detection despite the Perl sigil overlap, alias coercion, malformed passthrough. - `tests/test_code_compressor_language_alias.py`: `php` in the canonical list, `phtml` in the alias table. - `docs/content/docs/code-compression.mdx`: PHP added to the Tier 2 supported-languages row. No new dependency: `tree-sitter-language-pack` (the existing `[code]` extra) already ships the PHP grammar. No Rust changes needed. ## Testing - [x] New unit tests added and passing - [x] Full affected test suites pass locally **Test Output** ``` $ python -m pytest tests/test_transforms/test_code_compressor.py tests/test_code_compressor_language_alias.py -q ============================= 120 passed in 7.81s ============================= $ python -m pytest tests/test_transforms/ -q 3 failed, 443 passed # the 3 failures (kompress ONNX thread caps, kompress size gate, # text_crusher unicode parity) reproduce identically on a clean # upstream/main checkout in this environment — pre-existing local # ONNX runtime quirks, unrelated to this change $ ruff check . (0.15.17, CI-pinned) → All checks passed! | ruff format --check → clean $ mypy headroom/transforms/code_compressor.py headroom/transforms/content_detector.py --ignore-missing-imports Success: no issues found in 2 source files ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.13, tree-sitter + tree-sitter-language-pack (<1.0) installed, branch `feat/201-php-code-compression` off `upstream/main`. - Exact command / steps: parsed PHP samples (namespaced class w/ methods, block-scoped namespace, mixed HTML+PHP) with `tree_sitter_language_pack.get_parser('php')` to verify every node name used in the config; then ran `CodeAwareCompressor().compress(php_code, language="php")` and `compress(php_code)` (auto-detection) on a 48-line realistic service class. - Observed result: explicit and auto-detected paths both return `language=CodeLanguage.PHP`, `compression_ratio=0.64`, `syntax_valid=True`; method bodies elided to `// [N lines omitted]` while `<?php`, `namespace`, `use` lines, class header, and all signatures are preserved verbatim in the original order. Before the detection fix, auto-detection returned `UNKNOWN` (Perl prefilter dominance) — reproduced and then verified fixed. - Not tested: exotic PHP shapes (heredoc-heavy code, attributes `#[...]` on methods, interleaved multi-`<?php ?>` HTML templates beyond the basic mixed case); these fall back to verbatim preservation via the uncaptured-node pass or malformed-passthrough, both of which are covered by tests for the simple cases. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-08-01 00:54:13 +02:00
@pytest.mark.skipif(not TREE_SITTER_INSTALLED, reason="tree-sitter grammar pack not installed")
class TestPhpSupport:
"""PHP (``php`` grammar) parity with C#: signatures preserved verbatim,
function/method bodies compressed, ``<?php`` tag and ``namespace``/``use``
header order preserved, Perl sigil-overlap disambiguated in detection,
malformed input passed through.
"""
def _compressor(self):
return CodeAwareCompressor(
CodeCompressorConfig(
min_tokens_for_compression=1,
max_body_lines=1,
enable_ccr=False,
)
)
def test_class_methods_compress_signatures_preserved(self):
code = (
"<?php\n"
"namespace App\\Service;\n"
"\n"
"use App\\Model\\User;\n"
"\n"
"final class UserService {\n"
" private $logger;\n"
"\n"
" public function process(User $u): bool {\n"
" $name = strtolower(trim($u->getName()));\n"
" $tags = [];\n"
" foreach ($u->getTags() as $tag) {\n"
" $tags[] = $tag->normalize();\n"
" }\n"
" $this->logger->info($name);\n"
" return true;\n"
" }\n"
"}\n"
)
result = self._compressor().compress(code, language="php")
assert result.language == CodeLanguage.PHP
assert result.syntax_valid is True
assert result.compression_ratio < 1.0
# signature + class header preserved verbatim
assert "final class UserService" in result.compressed
assert "public function process(User $u): bool" in result.compressed
# method body actually compressed
assert "lines omitted" in result.compressed
assert "$tag->normalize()" not in result.compressed
# the class is emitted exactly once
assert result.compressed.count("class UserService") == 1
def test_php_tag_and_namespace_precede_uses_and_types(self):
"""``<?php`` must stay first and ``namespace X;`` must precede the
``use`` imports and type declarations any other order is not valid
PHP."""
code = (
"<?php\n"
"namespace App\\Tools;\n"
"\n"
"use App\\Model\\Item;\n"
"\n"
"function helper(int $x): int {\n"
" $acc = 0;\n"
" for ($i = 0; $i < $x; $i++) {\n"
" $acc += $i;\n"
" $acc -= 1;\n"
" }\n"
" return $acc;\n"
"}\n"
)
result = self._compressor().compress(code, language="php")
assert result.language == CodeLanguage.PHP
assert result.syntax_valid is True
assert result.compression_ratio < 1.0
compressed = result.compressed
assert compressed.lstrip().startswith("<?php")
assert compressed.index("<?php") < compressed.index("namespace App\\Tools;")
assert compressed.index("namespace App\\Tools;") < compressed.index("use App\\Model\\Item;")
assert compressed.index("use App\\Model\\Item;") < compressed.index("function helper")
assert "lines omitted" in compressed
def test_detect_language_identifies_php(self):
"""Auto-detection recognizes PHP despite the Perl sigil overlap
(``$var`` matches Perl's prefilter; the ``<?php`` tag disambiguates)."""
code = (
"<?php\n"
"namespace Acme;\n"
"\n"
"use Acme\\Widget;\n"
"\n"
"class Svc {\n"
" public function add(int $a, int $b): int {\n"
" $sum = $a + $b;\n"
" return $sum;\n"
" }\n"
"}\n"
)
lang, confidence = detect_language(code)
assert lang == CodeLanguage.PHP
assert confidence > 0.0
def test_phtml_alias_coerces_to_php(self):
assert coerce_language("phtml") == CodeLanguage.PHP
assert coerce_language("php8") == CodeLanguage.PHP
def test_malformed_php_passes_through_unchanged(self):
code = "<?php\nclass Broken {\n public function oops( {\n"
result = self._compressor().compress(code, language="php")
assert result.compressed == code