mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
e90a98d321
* ci(eval-harness): render PR comment as verdict + table, fold raw JSON The eval-harness workflow pasted the entire eval_report.json into every PR comment as one raw fenced block. Replace that with a unit-tested display module (scripts/render_eval_comment.py): a one-line verdict (measured passed/total + pending count), a per-task markdown table (metric, value, threshold, result), and the full JSON folded into <details>. Display layer only — run_evals, scripts._eval_threshold_gate, the gate step, and the [eval-regression-acknowledged] ack contract are untouched. The row verdict mirrors the gate's failure signal (aggregate AND per-class, #328) so the table never shows a clean pass on a run the gate blocks. Registered in scripts/_ci_pytest_manifest.toml; workflow test extended to pin the renderer so the comment can't silently regress to a raw dump. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017jcH7gEkVTQ1baMbVedSZp * fix(eval-comment): escape pipes/newlines in table cells (codex P2) task_name / metric / comparison come from eval manifests; a `|` or newline in them would break the markdown table or spoof extra rows/results. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017jcH7gEkVTQ1baMbVedSZp * refactor(eval-comment): /simplify pass — \r line boundaries, fd hygiene, gate-agreement pin - _cell now normalizes ALL line boundaries via splitlines() (codex re-review: \r could still split a table row), not just \n. - main() reads the report via a context manager (ResourceWarning hygiene). - New test pins that _task_failures agrees with _eval_threshold_gate.failed_tasks on which tasks fail, so the deliberate display-side mirror of the gate's failure rule drifts loudly (CI fail) instead of silently rendering green on a blocked run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017jcH7gEkVTQ1baMbVedSZp --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
141 lines
5.2 KiB
Python
141 lines
5.2 KiB
Python
"""Tests for .github/workflows/eval-harness.yml (#184 Delta 3 / Fix 1).
|
|
|
|
These assert the *structure and honesty* of the gate workflow, not GitHub's
|
|
runtime behaviour: it must strict-load with no duplicate keys, its concurrency
|
|
group must carry github.event_name, every step must declare at most one run
|
|
block, there must be no dead code after an exit, the dispatched rq_framing
|
|
runner must be in the path filter, and the final gate step must enforce the
|
|
absolute threshold + ack contract (its name must not over-promise).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
WORKFLOW_PATH = REPO_ROOT / ".github" / "workflows" / "eval-harness.yml"
|
|
|
|
|
|
class _NoDuplicateKeyLoader(yaml.SafeLoader):
|
|
"""SafeLoader that raises on a duplicate mapping key (silent-clobber guard)."""
|
|
|
|
|
|
def _no_dup_construct_mapping(loader, node, deep=False):
|
|
mapping = {}
|
|
for key_node, value_node in node.value:
|
|
key = loader.construct_object(key_node, deep=deep)
|
|
if key in mapping:
|
|
raise yaml.constructor.ConstructorError(
|
|
None, None, f"duplicate key: {key!r}", key_node.start_mark
|
|
)
|
|
mapping[key] = loader.construct_object(value_node, deep=deep)
|
|
return mapping
|
|
|
|
|
|
_NoDuplicateKeyLoader.add_constructor(
|
|
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _no_dup_construct_mapping
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def raw() -> str:
|
|
return WORKFLOW_PATH.read_text(encoding="utf-8")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def workflow(raw):
|
|
# Strict load: any duplicate key anywhere in the file fails here.
|
|
return yaml.load(raw, Loader=_NoDuplicateKeyLoader)
|
|
|
|
|
|
def _steps(workflow):
|
|
return workflow["jobs"]["eval-harness"]["steps"]
|
|
|
|
|
|
def test_strict_loads_without_duplicate_keys(workflow):
|
|
assert workflow["name"] == "Eval Harness"
|
|
assert "jobs" in workflow
|
|
|
|
|
|
def test_concurrency_group_includes_event_name(workflow):
|
|
group = workflow["concurrency"]["group"]
|
|
assert "github.event_name" in group
|
|
assert workflow["concurrency"]["cancel-in-progress"] is True
|
|
|
|
|
|
def test_each_step_has_at_most_one_run_block(raw):
|
|
# A step mapping with two `run:` keys would be silently merged by a lax
|
|
# loader; the strict loader already rejects that, but assert run-block count
|
|
# never exceeds the step count as a second guard.
|
|
run_blocks = raw.count("\n run: ")
|
|
# Steps that use `run:` are at the 8-space indent under `steps:`.
|
|
assert run_blocks >= 2 # at least the harness step + the gate step
|
|
|
|
|
|
def test_no_dead_code_after_exit(raw):
|
|
# No `exit 0`/`exit 1` immediately followed (next non-blank line) by an echo
|
|
# in the same run block — dead code the old gate carried.
|
|
lines = raw.splitlines()
|
|
for i, line in enumerate(lines):
|
|
stripped = line.strip()
|
|
if stripped in ("exit 0", "exit 1"):
|
|
# find the next non-blank, non-comment line
|
|
for nxt in lines[i + 1:]:
|
|
s = nxt.strip()
|
|
if not s or s.startswith("#"):
|
|
continue
|
|
# allow shell block terminators / new steps, forbid echo
|
|
assert not s.startswith("echo "), (
|
|
f"dead echo after '{stripped}' at line {i+1}: {nxt!r}"
|
|
)
|
|
break
|
|
|
|
|
|
def test_rq_framing_runner_in_path_filter(raw):
|
|
# The harness dispatches to scripts/check_rq_framing_patterns.py, so a change
|
|
# there must re-trigger the workflow. Must appear in BOTH pull_request and
|
|
# push path lists.
|
|
assert raw.count('"scripts/check_rq_framing_patterns.py"') >= 2
|
|
|
|
|
|
def test_final_gate_enforces_threshold_and_ack(workflow):
|
|
steps = _steps(workflow)
|
|
gate = next(s for s in steps if s.get("name", "").startswith("Eval gate"))
|
|
# Name must describe what it does: absolute threshold + ack contract.
|
|
assert "threshold" in gate["name"] and "ack" in gate["name"]
|
|
body = gate["run"]
|
|
# It consumes the computed failed-task list (not a voluntary token only).
|
|
assert "FAILED_TASKS" in body or "failed_tasks" in body
|
|
# It enforces the ack token AND a same-repo open issue.
|
|
assert "[eval-regression-acknowledged]" in body
|
|
assert "${GITHUB_REPOSITORY}" in body or "$repo" in body
|
|
|
|
|
|
def test_harness_step_computes_failed_tasks(workflow):
|
|
steps = _steps(workflow)
|
|
run_step = next(s for s in steps if s.get("id") == "run")
|
|
body = run_step["run"]
|
|
# The absolute-threshold verdict is computed via the unit-tested module
|
|
# (not an inline heredoc) and written to GITHUB_OUTPUT.
|
|
assert "scripts._eval_threshold_gate" in body
|
|
assert "failed_tasks=" in body
|
|
assert "GITHUB_OUTPUT" in body
|
|
|
|
|
|
def test_comment_built_by_renderer_module(workflow):
|
|
steps = _steps(workflow)
|
|
run_step = next(s for s in steps if s.get("id") == "run")
|
|
body = run_step["run"]
|
|
# The PR comment body is rendered by the unit-tested display module (not a
|
|
# raw `cat` of the report into a fenced block).
|
|
assert "scripts.render_eval_comment" in body
|
|
assert "eval_comment.md" in body
|
|
|
|
|
|
def test_workflow_has_no_inline_python_heredoc(raw):
|
|
# Fix 1 moved the gate logic into a module; no fragile inline python heredoc
|
|
# should remain in the workflow.
|
|
assert "<<'PY'" not in raw and '<<"PY"' not in raw
|