Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ uv run ruff check .
- **Regression tests must name the PR/commit they guard** in the docstring (e.g. `Guards the fix from PR #198 against the regression introduced by PR #193`).
- **Human review before `main`.** PRs only. No force-pushes to `main`. Self-approval doesn't count.
- **Trunk-based:** branch off `main`, PR back to `main`. No long-lived release branches.
- **Dependency cooldown:** `uv lock` never resolves to a release younger than 7 days. Don't hand-edit `[tool.uv] exclude-newer` — run `python tools/lock.py` to set it to `now - 7d` and re-lock; `tests/test_dep_cooldown.py` fails if the committed cutoff is younger than the window. A genuinely urgent fix gets a commented `[tool.uv.exclude-newer-package]` override.
- **Releases:** current mechanics live in [`docs/release.md`](./docs/release.md). Merges to `main` publish internal preview `.devN` builds after CI passes; public releases require a reviewed stable-version PR, a matching `v<version>` tag on `main`, then a bump back to the next `.dev0`.

## Experiment guidance (when using benchflow to run batch tasks experiments)
Expand Down
34 changes: 34 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ build-backend = "hatchling.build"
only-include = [
"src",
"tests",
# `tests/test_dep_cooldown.py` does `from tools.lock import ...`, so the
# sdist must ship `tools` too or the test suite fails to import from a
# source distribution.
"tools",
"README.md",
"CHANGELOG.md",
"LICENSE",
Expand Down Expand Up @@ -174,3 +178,33 @@ exclude = [
"src/benchflow/rewards/rubric_config.py",
"src/benchflow/experimental/mcp/reviewer_server.py",
]

[tool.uv]
# The per-package cooldown override (`exclude-newer-package` below) is only
# honored by uv >= 0.8.4. Pin a floor so an older uv can't silently ignore the
# override table and resolve a different (vulnerable / un-vetted) lock.
required-version = ">=0.8.4"
# Dependency cooldown (hard rule): never resolve to a release younger than ~7
# days. `exclude-newer` caps every `uv lock` at this timestamp. Don't hand-edit
# it — run `python tools/lock.py` to set it to `now - 7d` and re-lock in one
# step (the value stays static so CI's `uv sync --locked` is deterministic; the
# helper just rolls it forward). The CI check `tests/test_dep_cooldown.py` fails
# if this date is ever within the last 7 days.
#
# Security exception: a freshly-published CVE fix (younger than the cap) is
# allowed via a per-package override in `exclude-newer-package` below, with a
# comment recording why. Remove the override once the package is >= 7 days old.
exclude-newer = "2026-06-08T00:00:00Z"

[tool.uv.exclude-newer-package]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pin uv before using exclude-newer-package

In environments that still have uv 0.7.x, this new table is an unknown [tool.uv] key: uv sync --locked reports unknown field exclude-newer-package, ignores the lockfile cutoff as removed, and tries to re-resolve from PyPI instead of installing the committed lock. Since the repo only requires users to have uv and does not set tool.uv.required-version, local setup and any older CI image can fail or bypass the intended cooldown; pin/install a uv version that supports per-package cutoffs before relying on this syntax.

Useful? React with 👍 / 👎.

# Grandfathered exceptions for packages the repo already depends on that are
# still younger than the cooldown. Each entry should age out (be removed) once
# the package crosses 7 days old.
#
# litellm[proxy]==1.89.0 (2026-06-13) is an exact pin newer than the cap, so
# resolution can't proceed without allowing it through.
litellm = "2026-06-14T00:00:00Z"
# starlette 1.3.1 (2026-06-12) fixes CVE-2026-54282/54283 but is < 7 days old;
# allow it through so the cooldown doesn't roll back to the vulnerable 1.2.1.
# (aiohttp 3.14.1 / python-multipart 0.0.32 are already >= 7 days old.)
starlette = "2026-06-13T00:00:00Z"
63 changes: 63 additions & 0 deletions tests/test_dep_cooldown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Hard rule: the dependency cooldown — never resolve to a release younger than
a 7-day window.

Two layers, both evaluated against *today*:

1. `test_uv_exclude_newer_caps_resolution` — `[tool.uv] exclude-newer` is the
static cutoff `uv lock` applies at resolve time; it must be >= 7 days old so
the default resolution can't reach a brand-new release. Set it with
`python tools/lock.py` (computes `now - 7d` and re-locks).
2. `test_locked_packages_respect_cooldown` — the dynamic half: it reads the
`upload-time` uv bakes into `uv.lock` and fails if any *resolved* package is
younger than `now - 7d`. This tracks the current date with no stored date to
trust and no network calls.

A genuinely urgent (e.g. security) fix younger than the window is allowed only
via an explicit, commented `[tool.uv.exclude-newer-package]` override, which
exempts that package from layer 2.
"""

from __future__ import annotations

import datetime
import tomllib
from pathlib import Path

from tools.lock import COOLDOWN_DAYS, find_cooldown_violations

_PYPROJECT = Path(__file__).resolve().parents[1] / "pyproject.toml"
_LOCK = Path(__file__).resolve().parents[1] / "uv.lock"


def test_uv_exclude_newer_caps_resolution() -> None:
cfg = tomllib.loads(_PYPROJECT.read_text(encoding="utf-8"))
raw = cfg.get("tool", {}).get("uv", {}).get("exclude-newer")
assert raw, (
"[tool.uv] exclude-newer is missing from pyproject.toml — it enforces the "
f"{COOLDOWN_DAYS}-day dependency cooldown and must be set."
)
cutoff = datetime.datetime.fromisoformat(str(raw).replace("Z", "+00:00"))
if cutoff.tzinfo is None:
cutoff = cutoff.replace(tzinfo=datetime.UTC)
age = datetime.datetime.now(datetime.UTC) - cutoff
assert age >= datetime.timedelta(days=COOLDOWN_DAYS), (
f"[tool.uv] exclude-newer ({raw}) is only {age.days}d old; the dependency "
f"cooldown requires it to be >= {COOLDOWN_DAYS} days in the past. Re-lock "
"with `python tools/lock.py` when taking dependency updates."
)


def test_locked_packages_respect_cooldown() -> None:
cfg = tomllib.loads(_PYPROJECT.read_text(encoding="utf-8"))
exempt = set(cfg.get("tool", {}).get("uv", {}).get("exclude-newer-package", {}))
violations = find_cooldown_violations(
_LOCK.read_text(encoding="utf-8"),
exempt=exempt,
now=datetime.datetime.now(datetime.UTC),
)
assert not violations, (
f"uv.lock resolves packages younger than the {COOLDOWN_DAYS}-day cooldown "
"without a documented [tool.uv.exclude-newer-package] override:\n"
+ "\n".join(f" {n} {v} (uploaded {ts:%Y-%m-%d})" for n, v, ts in violations)
+ "\nRe-lock with `python tools/lock.py`, or add a commented override."
)
Comment on lines +50 to +87

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The error message uses age.days, which is the integer days component of the timedelta, not the total elapsed days rounded. For a gap like 6 days 23 h 50 min, it prints "6d old" while the assertion correctly sees it as still less than 7 days — the diagnostic makes the boundary look farther away than it is. Prefer age.total_seconds() / 86400 for accurate sub-day precision.

Suggested change
assert age >= datetime.timedelta(days=COOLDOWN_DAYS), (
f"[tool.uv] exclude-newer ({raw}) is only {age.days}d old; the dependency "
f"cooldown requires it to be >= {COOLDOWN_DAYS} days in the past. Bump it "
f"to an older date (>= {COOLDOWN_DAYS}d ago) when taking dependency updates."
)
assert age >= datetime.timedelta(days=COOLDOWN_DAYS), (
f"[tool.uv] exclude-newer ({raw}) is only {age.total_seconds() / 86400:.1f}d old; "
f"the dependency cooldown requires it to be >= {COOLDOWN_DAYS} days in the past. "
f"Bump it to an older date (>= {COOLDOWN_DAYS}d ago) when taking dependency updates."
)

122 changes: 122 additions & 0 deletions tests/test_lock_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Unit tests for the dependency-cooldown re-lock helper (`tools/lock.py`).

These cover the pure date/text transforms and the lock-freshness audit against
synthetic lock text; `uv lock` itself is not invoked.
"""

from __future__ import annotations

import datetime
import tomllib

import pytest

from tools.lock import (
COOLDOWN_DAYS,
LockError,
compute_cooldown_cutoff,
find_cooldown_violations,
newest_upload_times,
rewrite_exclude_newer,
)

_NOW = datetime.datetime(2026, 6, 15, 12, 0, tzinfo=datetime.UTC)

# floor (now - 7d) = 2026-06-08T12:00Z: old-pkg is older, fresh-pkg is younger.
_SYNTH_LOCK = """
[options]
exclude-newer = "2026-06-08T00:00:00Z"

[[package]]
name = "old-pkg"
version = "1.0.0"
sdist = { url = "https://example/old.tar.gz", upload-time = "2026-05-01T00:00:00Z" }

[[package]]
name = "fresh-pkg"
version = "2.0.0"
sdist = { url = "https://example/fresh.tar.gz", upload-time = "2026-06-13T00:00:00Z" }
wheels = [
{ url = "https://example/fresh.whl", upload-time = "2026-06-14T08:00:00Z" },
]

[[package]]
name = "git-pkg"
version = "0.1.0"
source = { git = "https://example/repo.git" }
"""


def test_cutoff_trails_today_by_cooldown_window_at_midnight() -> None:
now = datetime.datetime(2026, 6, 15, 9, 30, tzinfo=datetime.UTC)
assert compute_cooldown_cutoff(now) == "2026-06-08T00:00:00Z"


def test_cutoff_is_idempotent_within_a_day() -> None:
morning = datetime.datetime(2026, 6, 15, 0, 1, tzinfo=datetime.UTC)
night = datetime.datetime(2026, 6, 15, 23, 59, tzinfo=datetime.UTC)
assert compute_cooldown_cutoff(morning) == compute_cooldown_cutoff(night)


def test_cutoff_normalizes_non_utc_input() -> None:
# 2026-06-15 02:00 in UTC+9 is still 2026-06-14 17:00 UTC -> minus 7d.
tz = datetime.timezone(datetime.timedelta(hours=9))
now = datetime.datetime(2026, 6, 15, 2, 0, tzinfo=tz)
assert compute_cooldown_cutoff(now) == "2026-06-07T00:00:00Z"


def test_cutoff_default_window_is_the_cooldown_constant() -> None:
now = datetime.datetime(2026, 6, 15, 12, 0, tzinfo=datetime.UTC)
expected = (now - datetime.timedelta(days=COOLDOWN_DAYS)).date()
assert compute_cooldown_cutoff(now).startswith(expected.isoformat())


def test_cutoff_rejects_negative_window() -> None:
now = datetime.datetime(2026, 6, 15, tzinfo=datetime.UTC)
with pytest.raises(LockError):
compute_cooldown_cutoff(now, cooldown_days=-1)


def test_rewrite_replaces_only_the_exclude_newer_value() -> None:
text = (
"[tool.uv]\n"
'exclude-newer = "2026-06-01T00:00:00Z"\n'
"\n"
"[tool.uv.exclude-newer-package]\n"
'litellm = "2026-06-14T00:00:00Z"\n'
)
out = rewrite_exclude_newer(text, "2026-06-08T00:00:00Z")
assert 'exclude-newer = "2026-06-08T00:00:00Z"' in out
# The per-package override table is untouched.
assert 'litellm = "2026-06-14T00:00:00Z"' in out


def test_rewrite_requires_exactly_one_assignment() -> None:
with pytest.raises(LockError):
rewrite_exclude_newer("[tool.uv]\n", "2026-06-08T00:00:00Z")


def test_newest_upload_time_picks_latest_artifact_and_skips_sourceless() -> None:
newest = newest_upload_times(tomllib.loads(_SYNTH_LOCK))
# The wheel (08:00) beats the sdist (00:00) for the same package.
assert newest["fresh-pkg"][1] == datetime.datetime(
2026, 6, 14, 8, 0, tzinfo=datetime.UTC
)
# A git-sourced package has no upload-time and is omitted entirely.
assert "git-pkg" not in newest


def test_find_cooldown_violations_flags_only_young_unexempted_packages() -> None:
violations = find_cooldown_violations(_SYNTH_LOCK, exempt=set(), now=_NOW)
assert [name for name, _, _ in violations] == ["fresh-pkg"]


def test_find_cooldown_violations_honors_exemptions() -> None:
violations = find_cooldown_violations(_SYNTH_LOCK, exempt={"fresh-pkg"}, now=_NOW)
assert violations == []


def test_find_cooldown_violations_only_grows_more_lenient_over_time() -> None:
# The same lock, evaluated a month later: fresh-pkg has aged past the window.
later = _NOW + datetime.timedelta(days=30)
assert find_cooldown_violations(_SYNTH_LOCK, exempt=set(), now=later) == []
Loading
Loading