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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 6 * * 1"

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand Down
118 changes: 118 additions & 0 deletions .local/issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Audit Hardening Plan

Last updated: 2026-03-27
Branch: `audit-hardening-20260327`
Base: `main` at `95c7b4d7ca94993bcb0109de57fe81cedc8d711c`

## Scope

This document turns the validated audit findings into an executable implementation plan for the current repository state.

Out of scope for this task:
- PyPI/package-name distribution changes.
- Release-gate restructuring for GPU/full-dataset publication blocking.
- Architecture Decision Record authoring beyond incidental doc refreshes.

## Current Source Facts

- The repository already uses `tests/suites/{unit,integration,smoke}` with path-derived structural markers in `tests/conftest.py`.
- `CONTRIBUTING.md` still references pre-relocation root test paths for the API boundary lane.
- `.github/workflows/ci.yml` has no scheduled trigger.
- `ser/transcript/backends/faster_whisper.py` still owns real runtime and I/O branches that need stronger unit coverage.
- `ser/utils/common_utils.py` remains a tiny but uncovered utility dependency used by timeline rendering.

## Objectives

### 1. Weekly dependency-regression CI

Add a weekly scheduled run to `.github/workflows/ci.yml` so upstream dependency or resolver regressions surface even without active pull requests.

Acceptance criteria:
- `ci.yml` includes a weekly `schedule` trigger.
- Existing `changes` classification keeps scheduled runs on the full pipeline.
- Contributor docs mention the scheduled lane accurately.

### 2. Proper pytest marker governance

Strengthen test-marker discipline without relying on blanket marker edits. The repository already derives structural markers (`unit`, `integration`, `smoke`, `topology_contract`) from suite paths; the missing piece is contract enforcement around that policy and around non-structural marker ownership.

Implementation direction:
- Add architecture/contract tests inspired by `strongclaw/tests` to keep `tests/conftest.py` lean and structural.
- Assert that special-purpose markers such as `process_isolation` remain explicitly owned by the modules that require them.
- Keep structural suite markers path-derived, not hand-copied into every module.

Acceptance criteria:
- New contract tests fail if root bootstrap starts assigning non-structural markers.
- New contract tests fail if special-purpose markers move out of explicit module ownership.
- Test layout remains under `tests/suites/...` with domain-oriented placement.

### 3. Coverage uplift on runtime hotspots

Increase confidence in the least-tested real runtime code by adding focused unit tests for `FasterWhisperAdapter` and `display_elapsed_time`.

Implementation direction:
- Add a dedicated backend-focused test module under `tests/suites/unit/transcription/backends/`.
- Cover `setup_required`, `prepare_assets`, `load_model`, `transcribe`, and `_is_module_available` branches using fake modules and fake model objects.
- Add utility tests for `display_elapsed_time` under `tests/suites/unit/utils/`.

Acceptance criteria:
- New tests exercise successful and failure/edge branches for the faster-whisper adapter.
- Utility formatting behavior is covered for both long and short output styles.
- Overall branch coverage stays above the configured threshold with additional headroom.

### 4. Documentation refresh

Update contributor-facing docs to match the current suite tree, marker model, and CI behavior.

Acceptance criteria:
- `CONTRIBUTING.md` references `tests/suites/...` paths, not removed flat-root paths.
- CI topology text reflects scheduled CI and current suite/contract responsibilities.
- Any changed workflow/test semantics are documented where contributors would look first.

## Execution Checklist

- [x] Add weekly scheduled CI trigger and any supporting doc updates.
- [x] Add pytest bootstrap/marker contract tests under `tests/suites/integration/architecture/`.
- [x] Add dedicated faster-whisper adapter tests under `tests/suites/unit/transcription/backends/`.
- [x] Add `common_utils` coverage tests under `tests/suites/unit/utils/`.
- [x] Update `CONTRIBUTING.md` and nearby CI docs affected by the implementation.
- [x] Run focused validation while iterating.
- [x] Run full repo validation and compare outcomes against this document.
- [ ] Commit, push, open PR, merge once green, monitor workflows/branch freshness, then clean up worktree and local branches.

## Validation Plan

Focused checks during implementation:
- `uv run --extra dev pytest -q tests/suites/integration/architecture`
- `uv run --extra dev pytest -q tests/suites/unit/transcription/backends tests/suites/unit/utils/test_common_utils.py`
- `uv run --extra dev pytest --cov=ser.transcript.backends.faster_whisper --cov=ser.utils.common_utils --cov-branch --cov-report term-missing tests/suites/unit/transcription/backends tests/suites/unit/utils/test_common_utils.py`

Final required checks:
- `make lint`
- `make type`
- `make import-lint`
- `make test-cov`

## Validation Results

Focused validation completed:
- `uv run --extra dev pytest -q tests/suites/integration/architecture/test_pytest_suite_bootstrap.py tests/suites/unit/transcription/backends/test_faster_whisper_adapter.py tests/suites/unit/utils/test_common_utils.py` -> `24 passed`
- `uv run --extra dev pytest --cov=ser.transcript.backends.faster_whisper --cov=ser.utils.common_utils --cov-branch --cov-report term-missing tests/suites/unit/transcription/backends/test_faster_whisper_adapter.py tests/suites/unit/utils/test_common_utils.py` -> targeted coverage `80.81%`; `ser/transcript/backends/faster_whisper.py` reached `80.00%`

Final validation completed:
- `make lint` -> pass
- `make type` -> pass (`mypy` clean, `pyright` 0 errors / 0 warnings / 0 informations)
- `make import-lint` -> pass
- `make test-cov` -> pass with `986 passed` and total branch coverage `80.08%`

Outcome versus objectives:
- Weekly scheduled CI trigger added.
- Marker governance now has contract coverage around root bootstrap responsibilities and explicit special-marker ownership.
- Faster-whisper hotspot coverage materially improved and `common_utils` now has direct tests.
- Contributor docs now match the suite tree and current CI behavior.

## Notes

- Work only from the dedicated worktree at `/Users/juanpedrosugg/dev/github/ser-audit-hardening-20260327`.
- Keep branch/commit/PR naming neutral and project-scoped.
- If upstream `origin/main` changes during the task, re-check freshness before pushing and again before merging.
16 changes: 14 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,16 @@ Run the boundary lane whenever your change touches `ser/api.py`, `ser/_internal/

```bash
make import-lint
uv run pytest -q tests/test_import_lint_policy.py tests/test_api_import_boundary.py tests/test_api.py tests/test_cli.py
uv run pytest -q \
tests/suites/integration/architecture/test_import_lint_policy.py \
tests/suites/integration/architecture/test_api_import_boundary.py \
tests/suites/integration/api/test_api.py \
tests/suites/integration/cli/test_cli.py
```

## CI Topology
Default CI is defined in `.github/workflows/ci.yml`.
It runs on pull requests, pushes to `main`, and a weekly scheduled sweep every Monday at `06:00 UTC`.

Quality and validation lanes:
- `changes`: classifies pull requests so docs-only PRs can skip heavy jobs while `push` to `main` still runs the full pipeline.
Expand All @@ -72,8 +77,15 @@ Quality and validation lanes:
- `contract-gates`: deterministic contract lane on Python 3.12 (API boundary import-lint gate + transcription benchmark contract test).
- `build`: package build + metadata/wheel smoke checks.

## Test Suite Layout
- `tests/suites/unit`: narrow owner or helper behavior.
- `tests/suites/integration`: multi-module orchestration, boundary, or workflow coverage.
- `tests/suites/smoke`: cheap user-path workflow checks.
- Structural markers (`unit`, `integration`, `smoke`, `topology_contract`) come from suite placement in `tests/conftest.py`.
- Non-structural markers must stay explicitly declared by the modules that need them.

## Hardware Validation
Hardware-specific workflows are manual (`workflow_dispatch`):
Hardware-specific workflows are manual by default (`workflow_dispatch`) and are also reusable by release orchestration through `workflow_call`:
- [docs/ci/hardware-validation.md](docs/ci/hardware-validation.md)

Policy note:
Expand Down
9 changes: 6 additions & 3 deletions docs/ci/hardware-validation.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Hardware Validation Workflows

Manual hardware validation is intentionally separated from default CI.
Hardware validation is intentionally separated from default CI.
These workflows support manual execution via `workflow_dispatch` and release orchestration via
`workflow_call`.

## MPS (GitHub-hosted)

- Workflow: `.github/workflows/macos15-mps-validation.yml`
- Trigger: `workflow_dispatch`
- Triggers: `workflow_dispatch`, `workflow_call`
- Runner: `macos-15`
- Requirement: an Apple Silicon macOS 15 runner with MPS available.
- The workflow fails fast if `torch.backends.mps.is_available()` or
Expand All @@ -28,7 +30,7 @@ gh workflow run .github/workflows/macos15-mps-validation.yml \
## CUDA and XPU (Self-hosted)

- Workflow: `.github/workflows/linux-selfhosted-gpu-validation.yml`
- Trigger: `workflow_dispatch`
- Triggers: `workflow_dispatch`, `workflow_call`
- Runner: self-hosted Linux runners selected via JSON label inputs.
- Default CUDA labels: `["self-hosted","linux","x64","cuda"]`
- Default XPU labels: `["self-hosted","linux","x64","xpu"]`
Expand Down Expand Up @@ -75,4 +77,5 @@ gh workflow run .github/workflows/linux-selfhosted-gpu-validation.yml \
- Project support policy still includes local macOS13 validation targets:
- `darwin-x86_64-macos13-python3.12` (full profile support)
- `darwin-x86_64-macos13-python3.13` (partial, fast profile only)
- Default CI still does not call these workflows automatically.
- Keep hardware lanes manual unless runtime/cost constraints change.
125 changes: 125 additions & 0 deletions tests/suites/integration/architecture/test_pytest_suite_bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""Contracts for pytest suite bootstrap responsibilities and marker ownership."""

from __future__ import annotations

import ast
from pathlib import Path

import pytest

pytestmark = pytest.mark.topology_contract

_ROOT_CONFTEST = Path("tests/conftest.py")
_ALLOWED_ROOT_MARKERS = frozenset(
{
"integration",
"smoke",
"topology_contract",
"unit",
"usefixtures",
}
)
_IGNORED_MODULE_MARKERS = frozenset(
{
"filterwarnings",
"integration",
"smoke",
"topology_contract",
"unit",
"usefixtures",
}
)
_EXPECTED_SPECIAL_MARKERS = {
"tests/suites/integration/test_process_isolation.py": {"process_isolation"},
}


def _extract_marker_names(node: ast.expr) -> set[str]:
"""Extract marker names from a supported `pytestmark` expression."""
if isinstance(node, ast.Call):
return _extract_marker_names(node.func)
if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Attribute):
if isinstance(node.value.value, ast.Name) and node.value.value.id == "pytest":
if node.value.attr == "mark":
return {node.attr}
if isinstance(node, ast.List | ast.Tuple):
markers: set[str] = set()
for element in node.elts:
markers.update(_extract_marker_names(element))
return markers
raise AssertionError(f"Unsupported pytest marker expression: {ast.dump(node)}")


def _module_marker_names(path: Path) -> set[str]:
"""Return module-level pytest markers declared by one test module."""
tree = ast.parse(path.read_text(encoding="utf-8"), filename=path.as_posix())
for node in tree.body:
if not isinstance(node, ast.Assign):
continue
if len(node.targets) != 1 or not isinstance(node.targets[0], ast.Name):
continue
if node.targets[0].id != "pytestmark":
continue
return _extract_marker_names(node.value)
return set()


def _root_assigned_marker_names(path: Path) -> set[str]:
"""Return pytest marker names added dynamically by the root conftest."""
tree = ast.parse(path.read_text(encoding="utf-8"), filename=path.as_posix())
markers: set[str] = set()
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
if not isinstance(node.func, ast.Attribute) or node.func.attr != "add_marker":
continue
if len(node.args) != 1:
raise AssertionError("Root conftest marker injection must pass exactly one marker.")
markers.update(_extract_marker_names(node.args[0]))
return markers


def test_root_conftest_stays_lean(repo_root: Path) -> None:
"""Root suite bootstrap should stay small and structural."""
lines = (repo_root / _ROOT_CONFTEST).read_text(encoding="utf-8").splitlines()
assert len(lines) <= 80, f"tests/conftest.py grew to {len(lines)} lines (max 80)."


def test_root_conftest_registers_fixture_plugin_only(repo_root: Path) -> None:
"""Root bootstrap should expose shared fixtures via pytest_plugins only."""
tree = ast.parse((repo_root / _ROOT_CONFTEST).read_text(encoding="utf-8"))
plugin_values: tuple[str, ...] | None = None
for node in tree.body:
if not isinstance(node, ast.Assign):
continue
if len(node.targets) != 1 or not isinstance(node.targets[0], ast.Name):
continue
if node.targets[0].id != "pytest_plugins":
continue
if not isinstance(node.value, ast.Tuple):
raise AssertionError("pytest_plugins must stay a tuple of plugin module strings.")
plugin_values = tuple(
element.value
for element in node.value.elts
if isinstance(element, ast.Constant) and isinstance(element.value, str)
)
break

assert plugin_values == ("tests.fixtures.settings",)


def test_root_conftest_only_assigns_structural_markers(repo_root: Path) -> None:
"""Dynamic root marker injection should stay limited to suite semantics."""
assigned = _root_assigned_marker_names(repo_root / _ROOT_CONFTEST)
assert assigned == _ALLOWED_ROOT_MARKERS


def test_special_markers_are_explicitly_owned_by_modules(repo_root: Path) -> None:
"""Special-purpose pytest markers should stay declared by the owning module."""
discovered: dict[str, set[str]] = {}
for module_path in sorted((repo_root / "tests" / "suites").rglob("test_*.py")):
marker_names = _module_marker_names(module_path) - _IGNORED_MODULE_MARKERS
if marker_names:
discovered[module_path.relative_to(repo_root).as_posix()] = marker_names

assert discovered == _EXPECTED_SPECIAL_MARKERS
Loading