fix(e2e): stage the seeded device id per thread, not per process

Build 232 took two compat cells red with a FileNotFoundError renaming
`.claude.json.197` onto `.claude.json`. `run_claude_models_parallel`
drives several models from one process, so a pid-suffixed staged name is
shared between threads: one thread renamed the file the other was still
writing, and the loser died on a path that no longer existed.

mkstemp in the same directory gives a name that is unique per thread as
well as per process, and the rename stays atomic.
This commit is contained in:
Yuneng Jiang 2026-09-16 13:47:07 -07:00
parent 2481146727
commit 9421b26bf6
No known key found for this signature in database
2 changed files with 26 additions and 4 deletions

View file

@ -33,7 +33,7 @@ from typing import List, Tuple
import pytest
from claude_code.cli_driver import _stable_cli_state, run_claude
from claude_code.cli_driver import _FIXED_CLI_USER_ID, _seed_cli_identity, _stable_cli_state, run_claude
from claude_code.rate_limiter import RateLimiter
_STUB_REPLY = {
@ -141,3 +141,20 @@ def test_concurrent_cells_do_not_collide_on_the_pinned_session(
assert codes == [0, 0, 0, 0]
assert bodies, "the CLI sent no request to the stub, so there is nothing to compare"
assert set(Counter(bodies).values()) == {4}
def test_seeding_the_device_id_survives_threads_racing_on_the_same_directory(tmp_path: Path) -> None:
"""`run_claude_models_parallel` drives several models from one process, so the
seed's staged file has to be unique per thread and not merely per process."""
config_dir = tmp_path / "config"
config_dir.mkdir()
seeded = config_dir / ".claude.json"
for _round in range(20):
seeded.unlink(missing_ok=True)
with ThreadPoolExecutor(max_workers=16) as pool:
for outcome in [pool.submit(_seed_cli_identity, str(config_dir)) for _ in range(16)]:
outcome.result()
assert json.loads(seeded.read_text(encoding="utf-8"))["userID"] == _FIXED_CLI_USER_ID
assert sorted(entry.name for entry in config_dir.iterdir()) == [".claude.json"]

View file

@ -143,7 +143,12 @@ def _seed_cli_identity(config_dir: str) -> None:
`userID`, and sends them in `metadata.user_id` forever after, so the value
is stable for exactly as long as that file lives. Pinning it, and the
session id passed beside it, costs nothing: both feed abuse detection
rather than quota, caching or continuity."""
rather than quota, caching or continuity.
The staged name has to be unique per *thread*, not per process:
`run_claude_models_parallel` drives several models from one process, so a
pid-suffixed name lets one thread rename the file another is still
writing, and the loser dies on a missing path."""
path = os.path.join(config_dir, ".claude.json")
try:
with open(path, encoding="utf-8") as handle:
@ -151,8 +156,8 @@ def _seed_cli_identity(config_dir: str) -> None:
return
except (OSError, ValueError):
pass
staged = f"{path}.{os.getpid()}"
with open(staged, "w", encoding="utf-8") as handle:
handle_fd, staged = tempfile.mkstemp(dir=config_dir, prefix=".claude.json.")
with os.fdopen(handle_fd, "w", encoding="utf-8") as handle:
json.dump({"userID": _FIXED_CLI_USER_ID}, handle)
os.replace(staged, path)