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
63 changes: 63 additions & 0 deletions .github/workflows/javascript-coverage-quality-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: JavaScript Coverage Gate Quality CI

on:
pull_request:
branches: [main]
paths:
- '.github/workflows/javascript-coverage-quality-ci.yml'
- 'scripts/ci/javascript_coverage_gate.py'
- 'tests/test_javascript_coverage_gate.py'
- 'tests/test_javascript_coverage_storybook_boundary.py'
permissions:
contents: read

concurrency:
group: javascript-coverage-quality-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
exact-head-coverage-contract:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Checkout exact source revision
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.14'
- name: Install exact hash-verified quality dependencies
env:
PIP_DISABLE_PIP_VERSION_CHECK: '1'
PIP_NO_INPUT: '1'
shell: bash --noprofile --norc -e -o pipefail {0}
run: |
cat >"${RUNNER_TEMP}/javascript-coverage-quality-requirements.txt" <<'EOF'
coverage==7.15.2 --hash=sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f
iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760
packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e
pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c
EOF
python -m pip install \
--only-binary=:all: \
--require-hashes \
-r "${RUNNER_TEMP}/javascript-coverage-quality-requirements.txt"
- name: Verify full central suite and classifier coverage
shell: bash --noprofile --norc -e -o pipefail {0}
run: |
test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha || github.sha }}"
python -m coverage run --branch -m pytest --import-mode=importlib tests -q
python -m coverage report \
--include='scripts/ci/javascript_coverage_gate.py' \
--show-missing \
--fail-under=100
python -m compileall -q \
scripts/ci/javascript_coverage_gate.py \
tests/test_javascript_coverage_gate.py \
tests/test_javascript_coverage_storybook_boundary.py
git diff --exit-code
4 changes: 3 additions & 1 deletion scripts/ci/javascript_coverage_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
METRICS = ("statements", "branches", "functions", "lines")
SOURCE_SUFFIXES = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
EXCLUDED_PARTS = {
".storybook",
"__tests__",
"coverage",
"dist",
Expand All @@ -23,6 +24,7 @@
"tests",
}
TEST_NAME_RE = re.compile(r"\.(?:spec|test)\.[cm]?[jt]sx?$")
STORY_NAME_RE = re.compile(r"\.stories\.[cm]?[jt]sx?$")
HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")


Expand Down Expand Up @@ -66,7 +68,7 @@ def is_runtime_source(path: str) -> bool:
name = normalized.name.casefold()
if normalized.suffix.casefold() not in SOURCE_SUFFIXES:
return False
if name.endswith(".d.ts") or TEST_NAME_RE.search(name):
if name.endswith(".d.ts") or TEST_NAME_RE.search(name) or STORY_NAME_RE.search(name):
return False
if lowered_parts & EXCLUDED_PARTS:
return False
Expand Down
66 changes: 66 additions & 0 deletions tests/test_javascript_coverage_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,3 +481,69 @@ def test_comment_only_change_without_mapped_units_passes(
report = capsys.readouterr().out
assert "comments, delimiters, or type-only declarations" in report
assert "Result: PASS" in report


def test_changed_runtime_path_without_added_hunks_is_ignored(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
completed = subprocess.CompletedProcess(
args=["git"], returncode=0, stdout=b"src/runtime.ts\0", stderr=b""
)
monkeypatch.setattr(gate.subprocess, "run", lambda *args, **kwargs: completed)
monkeypatch.setattr(gate, "git", lambda *args: "diff without an added hunk")
assert gate.changed_runtime_lines(tmp_path, "base", "head") == {}


def test_global_summary_ignores_invalid_statement_locations() -> None:
metrics = gate.summarize_final(
{
"src/runtime.ts": {
"s": {"invalid": 1, "valid": 1},
"f": {},
"b": {},
"statementMap": {
"invalid": {"start": {"line": "two"}},
"valid": {"start": {"line": 2}},
},
}
}
)
assert metrics["statements"] == 100.0
assert metrics["lines"] == 100.0


def test_path_normalization_falls_through_nonmatching_in_repo_paths(
tmp_path: Path,
) -> None:
changed = {"frontend/src/runtime.ts"}
assert gate.normalize_coverage_path(
str(tmp_path / "other.ts"), tmp_path, changed
) is None
assert gate.normalize_coverage_path("other.ts", tmp_path, changed) is None


def test_coverage_loader_accepts_absolute_paths_and_ignores_unknown_json(
tmp_path: Path,
) -> None:
final = tmp_path / "coverage-final.json"
other = tmp_path / "metadata.json"
final.write_text("{}", encoding="utf-8")
other.write_text("{}", encoding="utf-8")
listing = tmp_path / "coverage-files.txt"
listing.write_text(f"{final}\n{other}\n", encoding="utf-8")
summaries, finals = gate.load_coverage_files(tmp_path, listing)
assert summaries == []
assert finals == [(final, {})]


def test_unrelated_coverage_records_do_not_satisfy_changed_source(
tmp_path: Path, capsys
) -> None:
repo, base_sha, head_sha = fixture_repo(tmp_path)
summary_list = write_coverage(repo, statement_count=1)
final_path = repo / "coverage" / "coverage-final.json"
final_path.write_text(
json.dumps({str(repo / "src" / "other.ts"): {}}), encoding="utf-8"
)
assert run_gate(repo, base_sha, head_sha, summary_list) == 1
assert "src/calculate_total.ts is absent" in capsys.readouterr().out
37 changes: 37 additions & 0 deletions tests/test_javascript_coverage_storybook_boundary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Regression tests for Storybook development evidence classification."""

from __future__ import annotations

import pytest

from scripts.ci import javascript_coverage_gate as gate


@pytest.mark.parametrize(
"path",
[
".storybook/main.ts",
".storybook/preview.ts",
"packages/editor/.storybook/main.ts",
"stories/Foo.stories.tsx",
"src/components/Foo.stories.ts",
],
)
def test_storybook_development_evidence_is_not_product_runtime(path: str) -> None:
"""Storybook configuration and stories do not count as shipped runtime."""
assert not gate.is_runtime_source(path)


@pytest.mark.parametrize(
"path",
[
"src/foo.ts",
"stories/runtime.ts",
"src/Foo.story.tsx",
"src/Foo.stories-helper.ts",
"src/storybook/main.ts",
],
)
def test_nearby_and_near_miss_product_modules_remain_governed(path: str) -> None:
"""Story-adjacent product modules remain subject to runtime coverage."""
assert gate.is_runtime_source(path)
Loading