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
14 changes: 13 additions & 1 deletion scripts/worktree/new.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,19 @@ if ($LASTEXITCODE -ne 0) { throw "venv creation failed (is '$Python' on PATH?)"

Push-Location $WorktreePath
try {
& $venvPy -m pip install -e ".[$extras]"
# --constraint constraints.lock, exactly as EVERY install in ci.yml does. constraints.lock is the
# HASHLESS export of uv.lock kept in sync by the DEP-1 gate; without it pip RE-RESOLVES inside
# pyproject's ranges and a fresh worktree gets whatever PyPI serves today.
#
# Not hypothetical: a worktree created 2026-07-29 came up with ruff 0.16.0 while constraints.lock
# pinned 0.15.22. That worktree then reported ~829 findings CI does not have — and because the
# ruff pre-commit hook runs `ruff check --fix`, it REWROTE files to match, stripping `# noqa`
# directives the pinned ruff still wants. A venv that lints differently from CI does not merely
# mislead; it edits your source on commit.
#
# This is the same reasoning as the DEP-1 `--require-hashes` install, one rung down: CI proves the
# lock installs, this makes a developer's environment agree with it.
& $venvPy -m pip install --constraint constraints.lock -e ".[$extras]"
if ($LASTEXITCODE -ne 0) { throw "pip install -e .[$extras] failed (exit $LASTEXITCODE)" }
} finally {
Pop-Location
Expand Down
107 changes: 107 additions & 0 deletions tests/test_worktree_venv_constraint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 MessageFoundry Organization and contributors
"""A new worktree's venv must resolve to the SAME versions CI uses.

THE DEFECT THIS EXISTS FOR. ``scripts/worktree/new.ps1`` provisioned each worktree with a bare
``pip install -e ".[dev,harness]"``. Every install in ``ci.yml`` passes ``--constraint
constraints.lock`` — its own comment says why: *"Without it, `uv pip install -e ".[extras]"`
RE-RESOLVES"*. ``new.ps1`` was the one place that omitted it, so a worktree resolved freely inside
``pyproject``'s ranges and could differ from CI on any tool.

Measured 2026-07-29: a worktree came up with **ruff 0.16.0** while ``constraints.lock`` pinned
**0.15.22**. Two consequences, and the second is why this is a guard rather than a nicety:

* it reported ~829 findings CI does not have (0.16 enabled stricter defaults), and
* the ``ruff-check`` pre-commit hook runs ``ruff check --fix``, so it **rewrote files** to match —
stripping ``# noqa`` directives the pinned ruff still wants.

A venv that lints differently from CI does not merely mislead; it edits your source on commit.

WHY A TEXT TEST. Nothing in the suite can run ``new.ps1`` — it creates a git worktree and a venv, which
is minutes of I/O and mutates the repo's worktree list. The invariant that actually matters is a
property of the COMMAND, so it is asserted against the command. That is the same posture as
``tests/test_ci_venv_pinning.py``, which pins workflow install lines no test can execute either.
"""

from __future__ import annotations

import re
from pathlib import Path

_REPO = Path(__file__).resolve().parents[1]
_NEW_PS1 = _REPO / "scripts" / "worktree" / "new.ps1"
_CI = _REPO / ".github" / "workflows" / "ci.yml"
_CONSTRAINTS = _REPO / "constraints.lock"

#: A project install: ``pip install ... -e ".[extras]"``. Matches the editable-dot form specifically,
#: which is what installs THIS checkout; a lockfile install (``-r``) is a different thing and is not
#: what this module is about.
_EDITABLE_PROJECT_INSTALL = re.compile(r"pip\s+install\b[^\n]*-e\s+[\"']\.\[")


def _code_lines(path: Path) -> list[str]:
"""Non-comment lines. The rationale comments quote the very command under test, so scanning the
raw text would let an explanation satisfy — or trip — the assertion."""
return [
ln for ln in path.read_text(encoding="utf-8").splitlines() if not ln.strip().startswith("#")
]


def test_new_worktree_installs_against_the_constraint_lock() -> None:
"""The fix itself: every editable project install in new.ps1 pins to constraints.lock."""
installs = [ln for ln in _code_lines(_NEW_PS1) if _EDITABLE_PROJECT_INSTALL.search(ln)]

# Non-vacuity. If the provisioning is restructured away, fail loudly rather than pass by finding
# nothing to check — the failure mode that turns a guard into decoration.
assert installs, (
f'{_NEW_PS1.name} no longer contains an editable project install (`pip install -e ".[...]"`). '
"If provisioning moved, re-point this guard at wherever it went."
)
unconstrained = [ln.strip() for ln in installs if "--constraint" not in ln]
assert not unconstrained, (
"a worktree venv is provisioned WITHOUT --constraint constraints.lock:\n "
+ "\n ".join(unconstrained)
+ "\nThat lets pip re-resolve inside pyproject's ranges, so the worktree can lint, typecheck "
"and test against different tool versions than CI. It has already happened: ruff 0.16.0 "
"against a lock pinning 0.15.22, whose `ruff check --fix` hook then rewrote files."
)


def test_the_constraint_file_new_ps1_names_actually_exists() -> None:
"""A pin at a path that does not exist fails the install outright — on a developer's first run.

Cheap to assert, and it is the difference between this change working and it being a typo that
nobody notices until someone creates a worktree.
"""
assert _CONSTRAINTS.is_file(), (
f"{_NEW_PS1.name} passes `--constraint constraints.lock`, but {_CONSTRAINTS} does not exist. "
"Either the export path moved (see the DEP-1 step in security.yml) or the flag is wrong."
)
body = _CONSTRAINTS.read_text(encoding="utf-8")
pinned = re.findall(r"(?m)^([A-Za-z0-9._-]+)==", body)
assert len(pinned) > 50, (
f"constraints.lock parsed to only {len(pinned)} pinned distributions — it is meant to be the "
"hashless export of the whole resolved set. A near-empty constraint file silently constrains "
"almost nothing, which is the same outcome as omitting the flag."
)


def test_new_ps1_matches_the_posture_ci_documents() -> None:
"""CI is the reference. If it stops constraining, this guard is enforcing a stale rule.

Asserted in this direction on purpose: the point is not "new.ps1 contains a flag", it is
"developer provisioning agrees with CI provisioning". Should CI ever move off constraints.lock,
this fails and forces the two to be reconciled deliberately rather than drifting apart again.
"""
ci_installs = [ln for ln in _code_lines(_CI) if _EDITABLE_PROJECT_INSTALL.search(ln)]
assert ci_installs, (
"ci.yml no longer contains an editable project install — re-point this comparison rather than "
"letting it pass on an empty set."
)
ci_unconstrained = [ln.strip() for ln in ci_installs if "--constraint" not in ln]
assert not ci_unconstrained, (
"ci.yml has an editable project install without --constraint:\n "
+ "\n ".join(ci_unconstrained)
+ "\nIf that is deliberate, new.ps1's constraint is now enforcing a rule CI no longer follows; "
"reconcile the two in one change."
)
Loading