release: 0.53.2 — diagnose silent-capture check + urllib3 2.7.0 + flaky-test fix (#270)
Three bundled improvements: - #244 — new `agnes diagnose` check compares SessionStart events (~/.claude/projects/<encoded>/*.jsonl) against agnes-push uploaded log entries inside a 7-day window. Surfaces a warning when the gap exceeds 3, hinting at silently-broken capture-session — previously detectable only weeks after the fact. - Dependabot — bumps transitive urllib3 from 1.26.20 to 2.7.0 to close 5 advisories (4 high, 1 medium). kbcstorage 0.9.5 still pins urllib3<2.0.0 upstream; overridden via [tool.uv] override-dependencies since the SDK works fine against 2.x in practice (Client + Tables both flow through requests, which supports both lines). - #252 — fix flaky test_scratch_dir_cleaned_up_after_failed_extraction by redirecting tempfile.tempdir to a per-test tmp_path. Pre-#252 the test scanned the shared system tmp dir and a sibling store test in another pytest-xdist worker could trip the assertion mid-window. Closes #244. Closes #252.
This commit is contained in:
parent
ea6fcfda3b
commit
c8de0e0f64
7 changed files with 1647 additions and 1257 deletions
20
CHANGELOG.md
20
CHANGELOG.md
|
|
@ -10,6 +10,26 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.53.3] — 2026-05-12
|
||||
|
||||
Hygiene round closing #244 + #252 + clearing 5 Dependabot urllib3 advisories. (Originally cut as 0.53.2 — bumped to 0.53.3 after #264 / #268 landed as 0.53.2 in parallel.)
|
||||
|
||||
### Added
|
||||
|
||||
- **`agnes diagnose` flags silently-broken `agnes capture-session`** (#244). New check compares `~/.claude/projects/<encoded>/*.jsonl` (SessionStart events Claude Code wrote) against `<workspace>/.claude/agnes-sessions-uploaded.txt` (entries `agnes push` actually shipped) inside a 7-day window. If the gap exceeds 3 sessions, surfaces a `warning` status with both counts plus a `agnes capture-session --verbose` pointer for manual triage. Pre-#244 a stdin-contract change in Claude Code would silently stop session uploads with the only observable signal being "session uploads stopped happening" — usually noticed weeks later.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`urllib3` bumped from 1.26.20 to 2.7.0** to close 5 Dependabot advisories (4 high, 1 medium): cross-origin sensitive-header leak on proxied low-level redirects, decompression-bomb safeguard bypass + unbounded decompression chain on the streaming API, and redirects-when-retries-disabled. `kbcstorage` 0.9.5 still declares `urllib3<2.0.0` upstream as of this release; we override it via `[tool.uv] override-dependencies` because the SDK works fine against 2.x in practice (we only use `Client` + `Tables`, both go through `requests`, which natively supports both lines). Keboola client + connector test paths exercised against 2.7.0 — no regressions.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`test_scratch_dir_cleaned_up_after_failed_extraction` no longer flakes under pytest-xdist** (#252). Pre-#252 the test scanned `tempfile.gettempdir()` for `agnes_store_*` directories and asserted the set hadn't grown across a request — but with `-n auto` workers a sibling store test in another worker could be mid-creation of its own `agnes_store_*` inside the [before, after] window, flipping the assertion. Test now redirects `tempfile.tempdir` to a per-test `tmp_path` so the glob only sees this test's scratch dir.
|
||||
|
||||
### Internal
|
||||
|
||||
- 8 regression tests in `tests/test_session_health.py` cover the #244 check matrix (ok / warning / info / threshold / window-bounds / malformed-log resilience).
|
||||
|
||||
## [0.53.2] — 2026-05-12
|
||||
|
||||
Two threads in one cut. **Operator surface:** `instance.brand` /
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
"""Diagnose command — agnes diagnose."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
|
||||
from cli.client import api_get
|
||||
from cli.config import get_sync_state
|
||||
from cli.lib.session_health import capture_session_health
|
||||
|
||||
diagnose_app = typer.Typer(help="System diagnostics")
|
||||
|
||||
|
|
@ -56,6 +58,14 @@ def diagnose(
|
|||
except Exception as e:
|
||||
checks.append({"name": "api", "status": "error", "detail": str(e)})
|
||||
|
||||
# Issue #244: detect silently-broken capture-session by comparing
|
||||
# observed SessionStart files against the uploaded-log entries.
|
||||
# Adds one entry to `checks` with status ok / warning / info.
|
||||
try:
|
||||
checks.append(capture_session_health(Path.cwd()))
|
||||
except Exception as e:
|
||||
checks.append({"name": "capture-session", "status": "info", "detail": f"health check failed: {e}"})
|
||||
|
||||
# Determine overall — `info` and `unknown` surface in the per-check
|
||||
# output but never promote the headline (issue #178).
|
||||
overall = "healthy"
|
||||
|
|
@ -74,6 +84,12 @@ def diagnose(
|
|||
if c.get("stale_tables"):
|
||||
for t in c["stale_tables"]:
|
||||
actions.append(f"Table '{t}' is stale. Run: agnes server logs scheduler | grep {t}")
|
||||
if c["name"] == "capture-session" and c["status"] == "warning":
|
||||
actions.append(
|
||||
"Capture-session may be silently failing. Run "
|
||||
"`agnes capture-session --verbose < ~/.claude/projects/<encoded>/<session>.jsonl` "
|
||||
"against a recent session file to surface the real error."
|
||||
)
|
||||
|
||||
result = {
|
||||
"overall": overall,
|
||||
|
|
|
|||
136
cli/lib/session_health.py
Normal file
136
cli/lib/session_health.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
"""Health check: detect silently-broken `agnes capture-session`.
|
||||
|
||||
Issue #244. `agnes capture-session` (the SessionStart hook helper)
|
||||
exits 0 on every failure mode so the hook is invisible during session
|
||||
startup. If Claude Code changes its stdin contract or capture-session
|
||||
crashes mid-write, the uploaded-log stops growing — but the SessionStart
|
||||
events keep landing in `~/.claude/projects/<encoded>/`. The gap between
|
||||
the two is a passive signal we can surface in `agnes diagnose`.
|
||||
|
||||
The check compares:
|
||||
|
||||
1. **Expected** — count of session jsonl files in every
|
||||
``~/.claude/projects/<encoded>/`` matching the current workspace with
|
||||
``mtime`` within the configured window.
|
||||
|
||||
2. **Actual** — count of entries in
|
||||
``<workspace>/.claude/agnes-sessions-uploaded.txt`` whose
|
||||
``<iso_timestamp>`` prefix falls within the same window.
|
||||
|
||||
If ``expected - actual`` exceeds the threshold, capture-session is
|
||||
likely broken end-to-end. Emit a ``warning`` with both counts plus a
|
||||
pointer to ``agnes capture-session --verbose`` for manual triage.
|
||||
|
||||
Window and threshold are conservative defaults (7d / 3) tuned to
|
||||
surface stop-the-world breakage without false-positive churn on a
|
||||
fresh workspace. Callers can override via ``window_days`` /
|
||||
``threshold`` kwargs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from cli.lib.claude_sessions import find_claude_sessions_dirs
|
||||
from cli.lib.session_queue import uploaded_log_path
|
||||
|
||||
_DEFAULT_WINDOW_DAYS = 7
|
||||
_DEFAULT_THRESHOLD = 3
|
||||
|
||||
|
||||
def _parse_uploaded_log_count(log_path: Path, cutoff: datetime) -> int:
|
||||
if not log_path.exists():
|
||||
return 0
|
||||
count = 0
|
||||
try:
|
||||
text = log_path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return 0
|
||||
for line in text.splitlines():
|
||||
ts_str, sep, _ = line.partition("\t")
|
||||
if not sep:
|
||||
continue
|
||||
try:
|
||||
ts = datetime.strptime(ts_str.strip(), "%Y-%m-%dT%H:%M:%SZ").replace(
|
||||
tzinfo=timezone.utc
|
||||
)
|
||||
except ValueError:
|
||||
continue
|
||||
if ts >= cutoff:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def _count_recent_session_files(workspace: Path, cutoff: datetime) -> int:
|
||||
count = 0
|
||||
for d in find_claude_sessions_dirs(workspace):
|
||||
try:
|
||||
iterator = d.glob("*.jsonl")
|
||||
except OSError:
|
||||
continue
|
||||
for f in iterator:
|
||||
try:
|
||||
mtime_ts = f.stat().st_mtime
|
||||
except OSError:
|
||||
continue
|
||||
mtime = datetime.fromtimestamp(mtime_ts, tz=timezone.utc)
|
||||
if mtime >= cutoff:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def capture_session_health(
|
||||
workspace: Path,
|
||||
window_days: int = _DEFAULT_WINDOW_DAYS,
|
||||
threshold: int = _DEFAULT_THRESHOLD,
|
||||
now: datetime | None = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Return a diagnose-shaped check dict for capture-session health.
|
||||
|
||||
Status values:
|
||||
|
||||
- ``ok`` — expected ≈ actual within threshold.
|
||||
- ``warning`` — observed SessionStart events that capture-session
|
||||
didn't write to the uploaded log; likely broken end-to-end.
|
||||
- ``info`` — no SessionStart events in the window (no signal).
|
||||
"""
|
||||
if now is None:
|
||||
now = datetime.now(timezone.utc)
|
||||
cutoff = now - timedelta(days=window_days)
|
||||
|
||||
expected = _count_recent_session_files(workspace, cutoff)
|
||||
actual = _parse_uploaded_log_count(uploaded_log_path(workspace), cutoff)
|
||||
delta = expected - actual
|
||||
|
||||
base: Dict[str, Any] = {
|
||||
"name": "capture-session",
|
||||
"expected_sessions": expected,
|
||||
"uploaded_entries": actual,
|
||||
"window_days": window_days,
|
||||
}
|
||||
|
||||
if expected == 0:
|
||||
return {
|
||||
**base,
|
||||
"status": "info",
|
||||
"detail": (
|
||||
f"no Claude Code sessions observed in the last {window_days}d "
|
||||
"for this workspace — nothing to verify"
|
||||
),
|
||||
}
|
||||
|
||||
if delta > threshold:
|
||||
return {
|
||||
**base,
|
||||
"status": "warning",
|
||||
"detail": (
|
||||
f"{expected} SessionStart event(s) in the last {window_days}d "
|
||||
f"but only {actual} entries in agnes-sessions-uploaded.txt — "
|
||||
"capture-session may be silently failing. Try: "
|
||||
"`agnes capture-session --verbose` against a session jsonl"
|
||||
),
|
||||
}
|
||||
|
||||
return {**base, "status": "ok"}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "agnes-the-ai-analyst"
|
||||
version = "0.53.2"
|
||||
version = "0.53.3"
|
||||
description = "Agnes — AI Data Analyst platform for AI analytical systems"
|
||||
requires-python = ">=3.11,<3.14"
|
||||
license = "MIT"
|
||||
|
|
@ -101,6 +101,14 @@ dependencies = [
|
|||
# a uniform API; OS releases the lock automatically on process exit (no
|
||||
# stale-lock detection required). Used by cli/lib/push_lock.py.
|
||||
"filelock>=3.13,<4",
|
||||
# Transitive dependency hardened directly to dodge 5 dependabot advisories
|
||||
# (4 high, 1 medium) flagged on urllib3<2.7.0: cross-origin sensitive
|
||||
# header leak on proxied low-level redirects, decompression-bomb bypass
|
||||
# + unbounded decompression chain on the streaming API, redirects-when-
|
||||
# retries-disabled. Forced via `[tool.uv] override-dependencies` below
|
||||
# because kbcstorage<=0.9.5 still pins urllib3<2.0.0 even though
|
||||
# botocore/requests/google-cloud-* all support 2.x on Python 3.10+.
|
||||
"urllib3>=2.7.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
|
@ -148,3 +156,11 @@ dev-dependencies = [
|
|||
"openai>=1.30.0",
|
||||
"fastapi-debug-toolbar>=0.6.3",
|
||||
]
|
||||
# Override the urllib3<2.0.0 ceiling kbcstorage 0.9.5 declares (upstream
|
||||
# hasn't relaxed it as of 2026-05-12 but the SDK works fine against
|
||||
# urllib3 2.x in practice — we only use `Client` + `Tables` from it and
|
||||
# both go through `requests`, which natively supports both lines). Lets
|
||||
# the resolver pick a urllib3 line that closes Dependabot advisories
|
||||
# CVE-2024-37891 / CVE-2025-{xxx}. See `urllib3>=2.7.0` in [project]
|
||||
# dependencies above for the security rationale.
|
||||
override-dependencies = ["urllib3>=2.7.0"]
|
||||
|
|
|
|||
206
tests/test_session_health.py
Normal file
206
tests/test_session_health.py
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
"""Regression coverage for cli.lib.session_health.capture_session_health.
|
||||
|
||||
Issue #244 — flag silently-broken `agnes capture-session` by comparing
|
||||
session files in `~/.claude/projects/<encoded>/` against entries in
|
||||
`<workspace>/.claude/agnes-sessions-uploaded.txt` within a sliding
|
||||
window.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _set_home(monkeypatch, tmp_path):
|
||||
"""Override the module-level ``_PROJECTS_DIR`` (evaluated once at
|
||||
import via ``Path.home()``) so the check reads from a controlled
|
||||
``~/.claude/projects/`` tree under tmp_path."""
|
||||
import cli.lib.claude_sessions as cs
|
||||
monkeypatch.setattr(cs, "_PROJECTS_DIR", tmp_path / ".claude" / "projects")
|
||||
|
||||
|
||||
def _make_session_file(home: Path, workspace: Path, name: str, age_days: float) -> Path:
|
||||
"""Write an empty jsonl into one of the candidate encoded dirs and
|
||||
backdate its mtime."""
|
||||
# Use variant-a encoding (slash→dash) — matches the macOS-friendly
|
||||
# form cli/lib/claude_sessions.py emits first.
|
||||
encoded = str(workspace.resolve()).replace("/", "-")
|
||||
target = home / ".claude" / "projects" / encoded
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
f = target / name
|
||||
f.write_text("{}\n", encoding="utf-8")
|
||||
# Backdate mtime
|
||||
age = time.time() - (age_days * 86400)
|
||||
os.utime(f, (age, age))
|
||||
return f
|
||||
|
||||
|
||||
def _append_uploaded_log(workspace: Path, when: datetime, transcript_path: str) -> None:
|
||||
(workspace / ".claude").mkdir(parents=True, exist_ok=True)
|
||||
log = workspace / ".claude" / "agnes-sessions-uploaded.txt"
|
||||
line = f"{when.strftime('%Y-%m-%dT%H:%M:%SZ')}\t{transcript_path}\n"
|
||||
with open(log, "a", encoding="utf-8") as f:
|
||||
f.write(line)
|
||||
|
||||
|
||||
def test_no_sessions_returns_info(tmp_path, monkeypatch):
|
||||
"""Fresh workspace with no SessionStart events → info, not warning."""
|
||||
workspace = tmp_path / "ws"
|
||||
workspace.mkdir()
|
||||
_set_home(monkeypatch, tmp_path / "home")
|
||||
(tmp_path / "home").mkdir()
|
||||
|
||||
from cli.lib.session_health import capture_session_health
|
||||
r = capture_session_health(workspace)
|
||||
assert r["status"] == "info"
|
||||
assert r["expected_sessions"] == 0
|
||||
assert r["uploaded_entries"] == 0
|
||||
|
||||
|
||||
def test_aligned_counts_returns_ok(tmp_path, monkeypatch):
|
||||
"""SessionStart events match uploaded-log entries → ok."""
|
||||
workspace = tmp_path / "ws"
|
||||
workspace.mkdir()
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
_set_home(monkeypatch, home)
|
||||
|
||||
# 3 recent sessions
|
||||
for i in range(3):
|
||||
_make_session_file(home, workspace, f"s{i}.jsonl", age_days=2)
|
||||
now = datetime.now(timezone.utc)
|
||||
for i in range(3):
|
||||
_append_uploaded_log(workspace, now - timedelta(days=2, hours=i),
|
||||
f"/path/s{i}.jsonl")
|
||||
|
||||
from cli.lib.session_health import capture_session_health
|
||||
r = capture_session_health(workspace)
|
||||
assert r["status"] == "ok"
|
||||
assert r["expected_sessions"] == 3
|
||||
assert r["uploaded_entries"] == 3
|
||||
|
||||
|
||||
def test_silent_breakage_returns_warning(tmp_path, monkeypatch):
|
||||
"""SessionStart events ≫ uploaded entries (delta > threshold) → warning."""
|
||||
workspace = tmp_path / "ws"
|
||||
workspace.mkdir()
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
_set_home(monkeypatch, home)
|
||||
|
||||
# 10 recent SessionStart events
|
||||
for i in range(10):
|
||||
_make_session_file(home, workspace, f"s{i}.jsonl", age_days=2)
|
||||
# only 2 uploads — capture-session silently dropped 8
|
||||
now = datetime.now(timezone.utc)
|
||||
for i in range(2):
|
||||
_append_uploaded_log(workspace, now - timedelta(days=1), f"/p{i}.jsonl")
|
||||
|
||||
from cli.lib.session_health import capture_session_health
|
||||
r = capture_session_health(workspace)
|
||||
assert r["status"] == "warning"
|
||||
assert r["expected_sessions"] == 10
|
||||
assert r["uploaded_entries"] == 2
|
||||
assert "capture-session may be silently failing" in r["detail"]
|
||||
|
||||
|
||||
def test_older_sessions_outside_window_ignored(tmp_path, monkeypatch):
|
||||
"""Sessions outside the window must not count toward expected."""
|
||||
workspace = tmp_path / "ws"
|
||||
workspace.mkdir()
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
_set_home(monkeypatch, home)
|
||||
|
||||
# 5 ancient sessions (60d ago) + 1 recent
|
||||
for i in range(5):
|
||||
_make_session_file(home, workspace, f"old{i}.jsonl", age_days=60)
|
||||
_make_session_file(home, workspace, "recent.jsonl", age_days=2)
|
||||
now = datetime.now(timezone.utc)
|
||||
_append_uploaded_log(workspace, now - timedelta(days=2), "/p/recent.jsonl")
|
||||
|
||||
from cli.lib.session_health import capture_session_health
|
||||
r = capture_session_health(workspace, window_days=7)
|
||||
assert r["status"] == "ok"
|
||||
assert r["expected_sessions"] == 1
|
||||
assert r["uploaded_entries"] == 1
|
||||
|
||||
|
||||
def test_uploaded_entries_outside_window_ignored(tmp_path, monkeypatch):
|
||||
"""Old uploaded-log entries don't count even if SessionStart count is high."""
|
||||
workspace = tmp_path / "ws"
|
||||
workspace.mkdir()
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
_set_home(monkeypatch, home)
|
||||
|
||||
for i in range(10):
|
||||
_make_session_file(home, workspace, f"s{i}.jsonl", age_days=1)
|
||||
# 8 uploads but ancient — outside window
|
||||
now = datetime.now(timezone.utc)
|
||||
for i in range(8):
|
||||
_append_uploaded_log(workspace, now - timedelta(days=60),
|
||||
f"/p{i}.jsonl")
|
||||
|
||||
from cli.lib.session_health import capture_session_health
|
||||
r = capture_session_health(workspace, window_days=7)
|
||||
assert r["status"] == "warning"
|
||||
assert r["expected_sessions"] == 10
|
||||
assert r["uploaded_entries"] == 0
|
||||
|
||||
|
||||
def test_threshold_respected(tmp_path, monkeypatch):
|
||||
"""Delta within threshold stays ok (a couple unsynced sessions is fine)."""
|
||||
workspace = tmp_path / "ws"
|
||||
workspace.mkdir()
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
_set_home(monkeypatch, home)
|
||||
|
||||
for i in range(5):
|
||||
_make_session_file(home, workspace, f"s{i}.jsonl", age_days=1)
|
||||
now = datetime.now(timezone.utc)
|
||||
# 3 uploads of 5 events → delta=2, threshold=3 → still ok
|
||||
for i in range(3):
|
||||
_append_uploaded_log(workspace, now - timedelta(days=1), f"/p{i}.jsonl")
|
||||
|
||||
from cli.lib.session_health import capture_session_health
|
||||
r = capture_session_health(workspace, window_days=7, threshold=3)
|
||||
assert r["status"] == "ok"
|
||||
assert r["expected_sessions"] == 5
|
||||
assert r["uploaded_entries"] == 3
|
||||
|
||||
|
||||
def test_malformed_uploaded_log_lines_skipped(tmp_path, monkeypatch):
|
||||
"""Garbage in uploaded-log doesn't crash the check; only well-formed
|
||||
timestamped lines count."""
|
||||
workspace = tmp_path / "ws"
|
||||
workspace.mkdir()
|
||||
(workspace / ".claude").mkdir()
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
_set_home(monkeypatch, home)
|
||||
|
||||
for i in range(3):
|
||||
_make_session_file(home, workspace, f"s{i}.jsonl", age_days=1)
|
||||
|
||||
log = workspace / ".claude" / "agnes-sessions-uploaded.txt"
|
||||
now = datetime.now(timezone.utc)
|
||||
log.write_text(
|
||||
"totally bogus line\n"
|
||||
"\n" # blank
|
||||
"no-tab-just-a-path\n"
|
||||
f"{(now - timedelta(days=1)).strftime('%Y-%m-%dT%H:%M:%SZ')}\t/p.jsonl\n"
|
||||
"not-a-timestamp\tstill-has-a-tab\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from cli.lib.session_health import capture_session_health
|
||||
r = capture_session_health(workspace, window_days=7, threshold=3)
|
||||
assert r["expected_sessions"] == 3
|
||||
assert r["uploaded_entries"] == 1
|
||||
|
|
@ -590,15 +590,26 @@ class TestStoreSecurityFixes:
|
|||
assert r2.status_code == 409, r2.text
|
||||
assert r2.json()["detail"] == "conflict_global_suffix"
|
||||
|
||||
def test_scratch_dir_cleaned_up_after_failed_extraction(self, web_client, monkeypatch):
|
||||
def test_scratch_dir_cleaned_up_after_failed_extraction(self, web_client, monkeypatch, tmp_path):
|
||||
"""Devin: ZIP-validation failure inside _safe_zip_extract was leaving
|
||||
the ``agnes_store_*`` scratch dir on disk because scratch creation
|
||||
and cleanup lived in different try/finally scopes. After the fix
|
||||
both share one outer try/finally; assert the dir really is gone.
|
||||
|
||||
Issue #252: redirect ``tempfile.mkdtemp()`` to a per-test ``tmp_path``
|
||||
via ``monkeypatch.setattr(tempfile, "tempdir", ...)`` so the
|
||||
``agnes_store_*`` glob is scoped to this test's exclusive directory.
|
||||
Pre-#252 the glob ran against the shared system tmp and would flake
|
||||
when a sibling pytest-xdist worker's store test happened to be
|
||||
mid-creation inside the [before, after] window.
|
||||
"""
|
||||
import tempfile as _tempfile
|
||||
from pathlib import Path as _Path
|
||||
|
||||
# FastAPI app runs in-process under TestClient → patching the
|
||||
# tempfile module here also redirects the server-side mkdtemp call.
|
||||
monkeypatch.setattr(_tempfile, "tempdir", str(tmp_path))
|
||||
|
||||
# A ZIP whose only member traverses out of the destination —
|
||||
# _safe_zip_extract raises 422 zip_unsafe_path before it touches
|
||||
# extractall. That's the simplest trigger that exits via
|
||||
|
|
@ -608,7 +619,7 @@ class TestStoreSecurityFixes:
|
|||
zf.writestr("../escape.txt", "boom")
|
||||
bad_zip = buf.getvalue()
|
||||
|
||||
tmp_root = _Path(_tempfile.gettempdir())
|
||||
tmp_root = tmp_path
|
||||
before = {p.name for p in tmp_root.glob("agnes_store_*")}
|
||||
|
||||
_, cookies = _create_user(web_client, "leak@x.com")
|
||||
|
|
|
|||
Loading…
Reference in a new issue