driftfix 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {driftfix-0.2.0 → driftfix-0.2.1}/CHANGELOG.md +5 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/PKG-INFO +30 -4
- {driftfix-0.2.0 → driftfix-0.2.1}/README.md +28 -3
- {driftfix-0.2.0 → driftfix-0.2.1}/pyproject.toml +2 -2
- {driftfix-0.2.0 → driftfix-0.2.1}/src/driftfix/cli.py +31 -11
- {driftfix-0.2.0 → driftfix-0.2.1}/tests/test_fix.py +38 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/.github/ISSUE_TEMPLATE/failed_fix.yml +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/.github/dependabot.yml +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/.github/workflows/ci.yml +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/.github/workflows/release.yml +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/.gitignore +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/CODE_OF_CONDUCT.md +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/CONTRIBUTING.md +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/LICENSE +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/SECURITY.md +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/action.yml +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/RESULTS.md +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/django/app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/django/case.json +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/django/pytest.ini +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/django/tests/conftest.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/django/tests/test_app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/flask/app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/flask/case.json +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/flask/pytest.ini +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/flask/tests/test_app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/httpx/app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/httpx/case.json +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/httpx/pytest.ini +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/httpx/tests/test_app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/numpy/app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/numpy/case.json +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/numpy/pytest.ini +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/numpy/tests/test_app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/openai/app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/openai/case.json +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/openai/pytest.ini +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/openai/tests/test_app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pandas/app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pandas/case.json +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pandas/pytest.ini +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pandas/tests/test_app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pillow/app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pillow/case.json +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pillow/pytest.ini +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pillow/tests/test_app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic/app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic/case.json +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic/pytest.ini +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic/tests/test_app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic-settings/app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic-settings/case.json +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic-settings/pytest.ini +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic-settings/requirements.txt +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic-settings/tests/test_app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic-shop/case.json +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic-shop/pytest.ini +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic-shop/shop/__init__.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic-shop/shop/base.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic-shop/shop/catalog.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic-shop/shop/models.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic-shop/shop/service.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic-shop/tests/test_models.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pydantic-shop/tests/test_service.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pyyaml/app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pyyaml/build-constraints.txt +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pyyaml/case.json +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pyyaml/pytest.ini +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/pyyaml/tests/test_app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/sqlalchemy/app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/sqlalchemy/case.json +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/sqlalchemy/pytest.ini +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/cases/sqlalchemy/tests/test_app.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/bench/run.py +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/pytest.ini +0 -0
- {driftfix-0.2.0 → driftfix-0.2.1}/src/driftfix/__init__.py +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
- Test commands no longer see `ANTHROPIC_API_KEY` or GitHub tokens
|
|
5
|
+
- Report the real cost when the agent stops on its budget (was $0.00)
|
|
6
|
+
- The pinning-back guard now allows widening the requirement to admit the new version (e.g. dropping `<2.0.0`)
|
|
7
|
+
|
|
3
8
|
## 0.2.0
|
|
4
9
|
- 5 more benchmark cases: pandas 2, Django 5, Flask 3, Pillow 10, PyYAML 6
|
|
5
10
|
- `driftfix --version`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: driftfix
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Dependabot bumps it. CI breaks. driftfix fixes the code.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Gthejesraj/driftfix
|
|
6
6
|
Project-URL: Issues, https://github.com/Gthejesraj/driftfix/issues
|
|
@@ -22,6 +22,7 @@ Classifier: Topic :: Software Development :: Build Tools
|
|
|
22
22
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
24
|
Requires-Dist: claude-agent-sdk>=0.2
|
|
25
|
+
Requires-Dist: packaging>=23
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
27
28
|
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
@@ -84,6 +85,23 @@ for free with `python bench/run.py --check`.
|
|
|
84
85
|
**Found an upgrade it can't fix? That's the most useful contribution:** see
|
|
85
86
|
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
86
87
|
|
|
88
|
+
### Real projects
|
|
89
|
+
|
|
90
|
+
The synthetic cases above are small. On real open-source projects broken by
|
|
91
|
+
pydantic 1 → 2 ([study](https://github.com/Gthejesraj/driftfix-study), 39
|
|
92
|
+
candidates → 4 real breaks):
|
|
93
|
+
|
|
94
|
+
| Project | Tests broken | Result | Cost |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| internetarchive/fatcat-scholar | 15 of 115 | ✅ fixed, 11 files | $2.38 |
|
|
97
|
+
| antonagestam/phantom-types | 2 of 677 | ✅ fixed, one test assertion edit to review | $2.93 |
|
|
98
|
+
| epi2me-labs/ezcharts | 5 of 37 | ✅ fixed, including the model generator | ~$1 |
|
|
99
|
+
| bdd100k/bdd100k | 2 of 46 | ⚠️ tests pass, but via a workaround for a third-party library | $0.95 |
|
|
100
|
+
|
|
101
|
+
Real fixes cost $1–3, not $0.20, so set `budget` to at least 3 for real
|
|
102
|
+
codebases. Always read the diff: "tests pass" and "correct" aren't the same
|
|
103
|
+
thing. Details in the study's [RESULTS.md](https://github.com/Gthejesraj/driftfix-study/blob/main/RESULTS.md).
|
|
104
|
+
|
|
87
105
|
## Quick start: GitHub Action
|
|
88
106
|
|
|
89
107
|
1. Add `.github/workflows/driftfix.yml`:
|
|
@@ -140,9 +158,10 @@ Exit codes: `0` fixed or nothing to fix, `1` not fixed, `2` dirty working tree.
|
|
|
140
158
|
|
|
141
159
|
- **Your tests decide.** driftfix reruns them after the agent finishes. The
|
|
142
160
|
agent saying it's done doesn't count.
|
|
143
|
-
- **No pinning back.**
|
|
144
|
-
lock
|
|
145
|
-
`
|
|
161
|
+
- **No pinning back.** A requirement that excludes the new version, or any
|
|
162
|
+
lock file edit, is rejected. Widening a requirement so the new version is
|
|
163
|
+
allowed (dropping an old `<2` cap) is fine, and so is adding a package the
|
|
164
|
+
upgrade split out (like `pydantic-settings`).
|
|
146
165
|
- **No deleting or skipping tests.** The agent is told not to, and every test
|
|
147
166
|
change shows up in the diff you review.
|
|
148
167
|
- **Reviewable.** It needs a clean git tree, so every change is a diff, and it
|
|
@@ -172,6 +191,13 @@ Native Renovate support is on the roadmap.
|
|
|
172
191
|
workflows from commits pushed with the default `GITHUB_TOKEN`. Pass a personal
|
|
173
192
|
access token or GitHub App token to `actions/checkout` if you want that.
|
|
174
193
|
|
|
194
|
+
**Can it fix deprecations before they break?** Yes. Make the test command
|
|
195
|
+
treat them as errors, e.g. `pytest -q -W error::DeprecationWarning` (some
|
|
196
|
+
libraries use their own warning class, like
|
|
197
|
+
`-W error::starlette.exceptions.StarletteDeprecationWarning`). On a real
|
|
198
|
+
31-file FastAPI app, this caught Starlette's `httpx` → `httpx2` deprecation and
|
|
199
|
+
fixed it with a one-line dependency swap ($0.40).
|
|
200
|
+
|
|
175
201
|
**Mocked tests?** driftfix can only catch what your tests catch. A test that
|
|
176
202
|
mocks the library can pass while the real call is broken.
|
|
177
203
|
|
|
@@ -55,6 +55,23 @@ for free with `python bench/run.py --check`.
|
|
|
55
55
|
**Found an upgrade it can't fix? That's the most useful contribution:** see
|
|
56
56
|
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
57
57
|
|
|
58
|
+
### Real projects
|
|
59
|
+
|
|
60
|
+
The synthetic cases above are small. On real open-source projects broken by
|
|
61
|
+
pydantic 1 → 2 ([study](https://github.com/Gthejesraj/driftfix-study), 39
|
|
62
|
+
candidates → 4 real breaks):
|
|
63
|
+
|
|
64
|
+
| Project | Tests broken | Result | Cost |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| internetarchive/fatcat-scholar | 15 of 115 | ✅ fixed, 11 files | $2.38 |
|
|
67
|
+
| antonagestam/phantom-types | 2 of 677 | ✅ fixed, one test assertion edit to review | $2.93 |
|
|
68
|
+
| epi2me-labs/ezcharts | 5 of 37 | ✅ fixed, including the model generator | ~$1 |
|
|
69
|
+
| bdd100k/bdd100k | 2 of 46 | ⚠️ tests pass, but via a workaround for a third-party library | $0.95 |
|
|
70
|
+
|
|
71
|
+
Real fixes cost $1–3, not $0.20, so set `budget` to at least 3 for real
|
|
72
|
+
codebases. Always read the diff: "tests pass" and "correct" aren't the same
|
|
73
|
+
thing. Details in the study's [RESULTS.md](https://github.com/Gthejesraj/driftfix-study/blob/main/RESULTS.md).
|
|
74
|
+
|
|
58
75
|
## Quick start: GitHub Action
|
|
59
76
|
|
|
60
77
|
1. Add `.github/workflows/driftfix.yml`:
|
|
@@ -111,9 +128,10 @@ Exit codes: `0` fixed or nothing to fix, `1` not fixed, `2` dirty working tree.
|
|
|
111
128
|
|
|
112
129
|
- **Your tests decide.** driftfix reruns them after the agent finishes. The
|
|
113
130
|
agent saying it's done doesn't count.
|
|
114
|
-
- **No pinning back.**
|
|
115
|
-
lock
|
|
116
|
-
`
|
|
131
|
+
- **No pinning back.** A requirement that excludes the new version, or any
|
|
132
|
+
lock file edit, is rejected. Widening a requirement so the new version is
|
|
133
|
+
allowed (dropping an old `<2` cap) is fine, and so is adding a package the
|
|
134
|
+
upgrade split out (like `pydantic-settings`).
|
|
117
135
|
- **No deleting or skipping tests.** The agent is told not to, and every test
|
|
118
136
|
change shows up in the diff you review.
|
|
119
137
|
- **Reviewable.** It needs a clean git tree, so every change is a diff, and it
|
|
@@ -143,6 +161,13 @@ Native Renovate support is on the roadmap.
|
|
|
143
161
|
workflows from commits pushed with the default `GITHUB_TOKEN`. Pass a personal
|
|
144
162
|
access token or GitHub App token to `actions/checkout` if you want that.
|
|
145
163
|
|
|
164
|
+
**Can it fix deprecations before they break?** Yes. Make the test command
|
|
165
|
+
treat them as errors, e.g. `pytest -q -W error::DeprecationWarning` (some
|
|
166
|
+
libraries use their own warning class, like
|
|
167
|
+
`-W error::starlette.exceptions.StarletteDeprecationWarning`). On a real
|
|
168
|
+
31-file FastAPI app, this caught Starlette's `httpx` → `httpx2` deprecation and
|
|
169
|
+
fixed it with a one-line dependency swap ($0.40).
|
|
170
|
+
|
|
146
171
|
**Mocked tests?** driftfix can only catch what your tests catch. A test that
|
|
147
172
|
mocks the library can pass while the real call is broken.
|
|
148
173
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "driftfix"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Dependabot bumps it. CI breaks. driftfix fixes the code."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -24,7 +24,7 @@ classifiers = [
|
|
|
24
24
|
"Topic :: Software Development :: Build Tools",
|
|
25
25
|
"Topic :: Software Development :: Quality Assurance",
|
|
26
26
|
]
|
|
27
|
-
dependencies = ["claude-agent-sdk>=0.2"]
|
|
27
|
+
dependencies = ["claude-agent-sdk>=0.2", "packaging>=23"]
|
|
28
28
|
|
|
29
29
|
[project.optional-dependencies]
|
|
30
30
|
dev = ["pytest>=8", "ruff>=0.6"]
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import asyncio
|
|
7
|
+
import os
|
|
7
8
|
import re
|
|
8
9
|
import subprocess
|
|
9
10
|
import sys
|
|
@@ -11,8 +12,13 @@ from dataclasses import dataclass
|
|
|
11
12
|
from importlib.metadata import version
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
15
|
+
from packaging.requirements import InvalidRequirement, Requirement
|
|
16
|
+
from packaging.version import InvalidVersion, Version
|
|
17
|
+
|
|
14
18
|
# Pinning back to the old version is not a fix. Adding a split-out package is fine.
|
|
15
19
|
MANIFESTS = {"requirements.txt", "pyproject.toml", "setup.py", "setup.cfg", "Pipfile", "package.json"}
|
|
20
|
+
# The test suite runs the upgraded package's code; it shouldn't see our credentials.
|
|
21
|
+
SECRETS = {"ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "GITHUB_TOKEN", "GH_TOKEN"}
|
|
16
22
|
LOCKFILES = {"Pipfile.lock", "poetry.lock", "uv.lock", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"}
|
|
17
23
|
|
|
18
24
|
PROMPT = """\
|
|
@@ -24,7 +30,8 @@ $ {test_command}
|
|
|
24
30
|
Fix this repository's code so it works with the new version of `{package}`.
|
|
25
31
|
- Read the package's changelog / migration guide for the relevant versions if you need to.
|
|
26
32
|
- Change application code. Change tests only where they use `{package}`'s API directly.
|
|
27
|
-
- Never pin
|
|
33
|
+
- Never pin or downgrade `{package}`, and never edit lock files. You may only widen its
|
|
34
|
+
requirement so the new version is allowed (e.g. drop an old upper bound).
|
|
28
35
|
- If the new version moved code into a separate package, you may add that package to the
|
|
29
36
|
dependency manifest and install it into the current environment.
|
|
30
37
|
- Never delete or skip tests to make them pass.
|
|
@@ -41,7 +48,8 @@ class TestRun:
|
|
|
41
48
|
def run_tests(command: str, repo: Path, timeout: int) -> TestRun:
|
|
42
49
|
try:
|
|
43
50
|
proc = subprocess.run(
|
|
44
|
-
command, shell=True, cwd=repo, capture_output=True, text=True, timeout=timeout
|
|
51
|
+
command, shell=True, cwd=repo, capture_output=True, text=True, timeout=timeout,
|
|
52
|
+
env={k: v for k, v in os.environ.items() if k not in SECRETS},
|
|
45
53
|
)
|
|
46
54
|
except subprocess.TimeoutExpired:
|
|
47
55
|
return TestRun(False, f"timed out after {timeout}s")
|
|
@@ -56,8 +64,17 @@ def changed_files(repo: Path) -> list[str]:
|
|
|
56
64
|
return [line[3:].split(" -> ")[-1] for line in out.splitlines()]
|
|
57
65
|
|
|
58
66
|
|
|
59
|
-
def
|
|
60
|
-
"""
|
|
67
|
+
def admits(line: str, new_version: str | None) -> bool:
|
|
68
|
+
"""Is this requirement line one that allows the upgraded version (e.g. a loosened cap)?"""
|
|
69
|
+
try:
|
|
70
|
+
req = Requirement(line.strip().strip("\"',"))
|
|
71
|
+
return new_version is not None and req.specifier.contains(Version(new_version), prereleases=True)
|
|
72
|
+
except (InvalidRequirement, InvalidVersion):
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def pins_package(repo: Path, path: str, package: str, new_version: str | None = None) -> bool:
|
|
77
|
+
"""Did this edit touch a lock file, or add a requirement for the package that excludes the new version?"""
|
|
61
78
|
if Path(path).name in LOCKFILES:
|
|
62
79
|
return True
|
|
63
80
|
if Path(path).name not in MANIFESTS:
|
|
@@ -65,13 +82,13 @@ def pins_package(repo: Path, path: str, package: str) -> bool:
|
|
|
65
82
|
diff = subprocess.run(
|
|
66
83
|
["git", "diff", "-U0", "--", path], cwd=repo, capture_output=True, text=True
|
|
67
84
|
).stdout
|
|
68
|
-
|
|
69
|
-
[line[1:] for line in diff.splitlines() if line[:1]
|
|
85
|
+
added = (
|
|
86
|
+
[line[1:] for line in diff.splitlines() if line[:1] == "+" and line[:3] != "+++"]
|
|
70
87
|
if diff else (repo / path).read_text().splitlines() # new, untracked file
|
|
71
88
|
)
|
|
72
89
|
names = "|".join(re.sub(r"[-_.]", "[-_.]", n) for n in re.split(r"[,\s]+", package) if n)
|
|
73
90
|
pattern = re.compile(rf"(?<![\w.-])({names})(?![\w.-])", re.IGNORECASE)
|
|
74
|
-
return any(pattern.search(line) for line in
|
|
91
|
+
return any(pattern.search(line) and not admits(line, new_version) for line in added)
|
|
75
92
|
|
|
76
93
|
|
|
77
94
|
def build_prompt(package: str, old: str | None, new: str | None, command: str, out: str) -> str:
|
|
@@ -95,9 +112,12 @@ async def run_agent(prompt: str, repo: Path, model: str, max_turns: int, budget:
|
|
|
95
112
|
skills=[],
|
|
96
113
|
)
|
|
97
114
|
summary, cost = "", 0.0
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
115
|
+
try:
|
|
116
|
+
async for message in query(prompt=prompt, options=options):
|
|
117
|
+
if isinstance(message, ResultMessage):
|
|
118
|
+
summary, cost = message.result or "", message.total_cost_usd or 0.0
|
|
119
|
+
except Exception as exc: # e.g. budget reached; the cost was already reported above
|
|
120
|
+
return f"Agent stopped: `{exc}`", cost
|
|
101
121
|
return summary, cost
|
|
102
122
|
|
|
103
123
|
|
|
@@ -120,7 +140,7 @@ def fix(args: argparse.Namespace) -> int:
|
|
|
120
140
|
summary, cost = f"Agent error: `{exc}`", 0.0
|
|
121
141
|
|
|
122
142
|
touched = [f for f in changed_files(repo) if f not in baseline]
|
|
123
|
-
pinned = [f for f in touched if pins_package(repo, f, args.package)]
|
|
143
|
+
pinned = [f for f in touched if pins_package(repo, f, args.package, args.to_version)]
|
|
124
144
|
after = run_tests(args.test, repo, args.timeout) # don't trust the agent's word
|
|
125
145
|
ok = after.passed and bool(touched) and not pinned
|
|
126
146
|
|
|
@@ -83,3 +83,41 @@ def test_pins_package():
|
|
|
83
83
|
assert pins_package(repo, "requirements.txt", "pydantic-core") # name normalization
|
|
84
84
|
assert not pins_package(repo, "requirements.txt", "pydantic") # pydantic-core is a different package
|
|
85
85
|
assert not pins_package(repo, "app.py", "pydantic")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_tests_cannot_see_api_key(tmp_path, monkeypatch):
|
|
89
|
+
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-secret")
|
|
90
|
+
leak = f"{sys.executable} -c 'import os, sys; sys.exit(\"ANTHROPIC_API_KEY\" in os.environ)'"
|
|
91
|
+
assert cli.run_tests(leak, tmp_path, 30).passed
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_cost_survives_agent_error(monkeypatch):
|
|
95
|
+
import asyncio
|
|
96
|
+
import sys as _sys
|
|
97
|
+
import types
|
|
98
|
+
|
|
99
|
+
class ResultMessage:
|
|
100
|
+
result, total_cost_usd = "", 1.02
|
|
101
|
+
|
|
102
|
+
async def query(**kw):
|
|
103
|
+
yield ResultMessage()
|
|
104
|
+
raise RuntimeError("Reached maximum budget ($1)")
|
|
105
|
+
|
|
106
|
+
fake = types.SimpleNamespace(
|
|
107
|
+
ClaudeAgentOptions=lambda **kw: None, ResultMessage=ResultMessage, query=query
|
|
108
|
+
)
|
|
109
|
+
monkeypatch.setitem(_sys.modules, "claude_agent_sdk", fake)
|
|
110
|
+
summary, cost = asyncio.run(cli.run_agent("p", Path("."), "m", 1, 1.0))
|
|
111
|
+
assert cost == 1.02 and "maximum budget" in summary
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_loosening_cap_is_allowed_pinning_back_is_not(tmp_path):
|
|
115
|
+
from driftfix.cli import pins_package
|
|
116
|
+
repo = make_repo(tmp_path, True)
|
|
117
|
+
(repo / "requirements.txt").write_text("lib>=2.0.0\n") # was lib==2, loosened
|
|
118
|
+
assert not pins_package(repo, "requirements.txt", "lib", "2.13.5")
|
|
119
|
+
(repo / "requirements.txt").write_text("lib<2\n") # pinned back
|
|
120
|
+
assert pins_package(repo, "requirements.txt", "lib", "2.13.5")
|
|
121
|
+
(repo / "requirements.txt").write_text("lib>=2.0.0\n") # unknown target version: be strict
|
|
122
|
+
assert pins_package(repo, "requirements.txt", "lib", None)
|
|
123
|
+
assert pins_package(repo, "requirements.txt", "lib", "1.x") # unparsable version: be strict
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|