Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,44 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install with dev extra
- name: Install with dev + authoring extras
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
python -m pip install -e ".[dev,authoring]"

- name: Run ruff
run: python -m ruff check src/ tests/

- name: Run tests
- name: Run tests (with coverage on ubuntu x py3.11)
run: |
if [ "${{ matrix.os }}" = "ubuntu-latest" ] && [ "${{ matrix.python-version }}" = "3.11" ]; then
python -m pytest tests/ -v --tb=short --cov --cov-report=term-missing --cov-report=xml
else
python -m pytest tests/ -v --tb=short
fi
shell: bash

- name: Upload coverage artifact
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11'
uses: actions/upload-artifact@v4
with:
name: coverage-attune-help
path: coverage.xml

test-no-authoring-extra:
name: zero-dep contract (no [authoring])
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
cache: pip
- name: Install with dev extra ONLY (no authoring)
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Run tests; shim tests must skip cleanly via importorskip
run: python -m pytest tests/ -v --tb=short
39 changes: 32 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,20 @@ classifiers = [
]
dependencies = [
"python-frontmatter>=1.0.0",
# Transitional: manifest/staleness/freshness moved to attune-author
# in 0.11.0. ``attune_help.manifest``/``staleness``/``freshness``
# are deprecated shims that re-export from attune-author. Required
# so existing imports keep working through the deprecation window.
# Drop this dep when the shims are removed (target 2026-07-07) and
# restore tech.md's zero-required-deps constraint.
"attune-author>=0.7.0",
]

[project.optional-dependencies]
rich = ["rich>=13.0.0"]
plugin = ["mcp>=0.9.0"]
# Transitional: manifest/staleness/freshness moved to attune-author in
# 0.11.0. ``attune_help.manifest``/``staleness``/``freshness`` are
# deprecated shims that re-export from attune-author. Install with
# ``pip install attune-help[authoring]`` to keep the shims working.
# Restored tech.md ADR-002 (zero required deps) on 2026-05-08.
# Sunset target: 2026-07-07 — drop the shims and this extra entirely.
authoring = [
"attune-author>=0.7.0",
]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
Expand All @@ -61,3 +63,26 @@ where = ["src"]

[tool.setuptools.package-data]
attune_help = ["templates/**/*.md", "templates/**/*.json", "demos/**/*.md"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra"
markers = [
"slow: tests that create real venvs or otherwise take >1s",
]

[tool.coverage.run]
source = ["src/attune_help"]
branch = true
omit = ["*/tests/*", "*/conftest.py"]

[tool.coverage.report]
show_missing = true
skip_covered = false
fail_under = 81
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"if __name__ == .__main__.:",
]
9 changes: 8 additions & 1 deletion src/attune_help/freshness/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@

import warnings

from attune_author.freshness import SymbolExtractor, SymbolRecord
try:
from attune_author.freshness import SymbolExtractor, SymbolRecord
except ImportError as exc: # pragma: no cover
raise ImportError(
"attune_help.freshness is a deprecated shim that requires the "
"'authoring' extra. Install with: pip install attune-help[authoring]. "
"Or migrate your imports to attune_author.freshness directly."
) from exc

__all__ = ["SymbolExtractor", "SymbolRecord"]

Expand Down
9 changes: 8 additions & 1 deletion src/attune_help/freshness/symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@

import warnings

from attune_author.freshness.symbols import SymbolExtractor, SymbolRecord
try:
from attune_author.freshness.symbols import SymbolExtractor, SymbolRecord
except ImportError as exc: # pragma: no cover
raise ImportError(
"attune_help.freshness.symbols is a deprecated shim that requires "
"the 'authoring' extra. Install with: pip install attune-help[authoring]. "
"Or migrate your imports to attune_author.freshness.symbols directly."
) from exc

__all__ = ["SymbolExtractor", "SymbolRecord"]

Expand Down
29 changes: 18 additions & 11 deletions src/attune_help/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,24 @@

import warnings

from attune_author.manifest import (
Feature,
FeatureManifest,
Manifest,
is_safe_feature_name,
load_manifest,
match_files_to_features,
resolve_topic,
save_manifest,
slugify,
)
try:
from attune_author.manifest import (
Feature,
FeatureManifest,
Manifest,
is_safe_feature_name,
load_manifest,
match_files_to_features,
resolve_topic,
save_manifest,
slugify,
)
except ImportError as exc: # pragma: no cover
raise ImportError(
"attune_help.manifest is a deprecated shim that requires the "
"'authoring' extra. Install with: pip install attune-help[authoring]. "
"Or migrate your imports to attune_author.manifest directly."
) from exc

__all__ = [
"Feature",
Expand Down
27 changes: 17 additions & 10 deletions src/attune_help/staleness.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,23 @@

import warnings

from attune_author.staleness import (
DocStaleness,
FeatureStaleness,
StalenessReport,
build_doc_footer,
check_staleness,
compute_semantic_hash,
compute_source_hash,
parse_doc_footer,
)
try:
from attune_author.staleness import (
DocStaleness,
FeatureStaleness,
StalenessReport,
build_doc_footer,
check_staleness,
compute_semantic_hash,
compute_source_hash,
parse_doc_footer,
)
except ImportError as exc: # pragma: no cover
raise ImportError(
"attune_help.staleness is a deprecated shim that requires the "
"'authoring' extra. Install with: pip install attune-help[authoring]. "
"Or migrate your imports to attune_author.staleness directly."
) from exc

__all__ = [
"DocStaleness",
Expand Down
61 changes: 61 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# attune-help tests

## Running locally

```bash
# Full suite (requires the deprecated authoring shims to resolve)
pip install -e ".[dev,authoring]"
pytest

# Zero-dep contract: shim tests skip via pytest.importorskip
pip install -e ".[dev]"
pytest

# With coverage (matches CI's ubuntu x py3.11 cell)
pip install -e ".[dev,authoring]"
pytest --cov --cov-report=term-missing
```

The `slow` marker covers tests that create real venvs
(`test_zero_dep_install.py`). Skip with `pytest -m "not slow"` for fast
iteration.

## LLM mocking standard

attune-help itself makes no LLM calls. Cross-layer integration tests that
*could* exercise an LLM follow the **attune-author reference pattern**:

- Strip `ANTHROPIC_API_KEY` via an autouse fixture so a misconfigured
test never reaches the network.
- Patch `anthropic.Anthropic` at import time, not at call site.
- Reset module-level singletons (e.g. `_RagPipeline`) between tests with
an autouse fixture so a leaked patch doesn't poison later tests.

See `attune-author/tests/conftest.py` (`_lenient_polish_by_default`,
`_reset_rag_pipeline`). Pass 2 of the test-strategy spec will formalize
this into a shared `docs/testing-conventions.md` across layers.

## What's tested vs. not

Tracked in
`/Users/patrickroebuck/attune/specs/test-strategy/current-state.md`. After
pass 1, the highest-value remaining gaps in this layer are:

- `cli.py` (~70% branch coverage) — argparse error paths
- `engine.py` (~86%) — depth-progression edge cases
- `transformers.py` (~78%) — channel-specific render branches

Pass 2 will revisit thresholds and target those areas if needed.

## Architectural contracts under test

- **`tests/test_zero_dep_install.py`** — guards
[tech.md](../../tech.md) ADR-002 (zero required deps beyond
`python-frontmatter`). Spins up a fresh venv per test case; marked
`@pytest.mark.slow`.
- **`tests/test_storage_protocol.py`** — reusable mixin verifying the
`SessionStorage` Protocol contract. New backends (Redis, DB) inherit
from `StorageProtocolTester` and override `_make_storage()`.
- **`tests/test_authoring_shims.py`** — verifies the deprecated re-export
shims still resolve correctly while the `[authoring]` extra is
installed. Sunset 2026-07-07.
64 changes: 64 additions & 0 deletions tests/test_adapter_rag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Tests for attune_help.adapters.rag — the attune-rag corpus adapter."""

from __future__ import annotations

from dataclasses import FrozenInstanceError
from pathlib import Path

import pytest

import attune_help
from attune_help.adapters.rag import AttuneHelpAdapter


def test_default_templates_root_points_to_bundled_dir() -> None:
adapter = AttuneHelpAdapter()
assert (
adapter.templates_root.is_dir()
), f"bundled templates dir missing: {adapter.templates_root}"
# Must live inside the installed package, not somewhere arbitrary.
package_root = Path(attune_help.__file__).resolve().parent
assert package_root in adapter.templates_root.resolve().parents


def test_default_version_matches_package_version() -> None:
adapter = AttuneHelpAdapter()
assert adapter.version == attune_help.__version__


def test_custom_root_and_version_override_defaults(tmp_path: Path) -> None:
custom_root = tmp_path / "custom-corpus"
custom_root.mkdir()
adapter = AttuneHelpAdapter(
templates_root=custom_root,
version="9.9.9-test",
)
assert adapter.templates_root == custom_root
assert adapter.version == "9.9.9-test"


def test_adapter_is_frozen() -> None:
adapter = AttuneHelpAdapter()
with pytest.raises(FrozenInstanceError):
adapter.version = "0.0.0" # type: ignore[misc]


def test_adapter_satisfies_help_corpus_adapter_protocol() -> None:
"""Structural conformance to attune-rag's HelpCorpusAdapter protocol.

The protocol requires ``templates_root: Path`` and ``version: str``.
We assert the shape directly instead of importing the protocol so
this test runs even when attune-rag isn't installed.
"""
adapter = AttuneHelpAdapter()
assert isinstance(adapter.templates_root, Path)
assert isinstance(adapter.version, str)
assert adapter.version # non-empty


def test_two_adapters_with_same_inputs_are_equal(tmp_path: Path) -> None:
a = AttuneHelpAdapter(templates_root=tmp_path, version="1.0")
b = AttuneHelpAdapter(templates_root=tmp_path, version="1.0")
assert a == b
# frozen dataclass instances are hashable.
assert hash(a) == hash(b)
2 changes: 2 additions & 0 deletions tests/test_authoring_shims.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

import pytest

pytest.importorskip("attune_author") # Shim tests require the [authoring] extra.

SHIMS = [
"attune_help.manifest",
"attune_help.staleness",
Expand Down
3 changes: 3 additions & 0 deletions tests/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
from pathlib import Path

import pytest

pytest.importorskip("attune_author") # Shim tests require the [authoring] extra.

from attune_help.manifest import (
Feature,
FeatureManifest,
Expand Down
Loading
Loading