Files
handler/tests/test_credsync.py
T
0xWheatyz 1517e4dca8 feat!: headless is the only runner - delete the tmux run path (phase 4)
Agent runs are now always worker-owned 'claude -p' subprocesses; tmux
survives only for the interactive /login flow.

- deleted: worker.capture_agent_output/_pane_tail + the capture loop
  arm (the empty-/log bug's home), spawn's tmux launch/_claude_command,
  the tmux resume/kill branches (the silent-send-keys bug's home),
  tmux.session_name/list_sessions, the CLI attach subcommand, the
  'runner' setting
- spawn: task is now a hard requirement (headless has no idle REPL) -
  enforced in spawn (SpawnError) and the API (400); onboarding seeding
  dropped (-p skips the trust dialog)
- resume: single headless path; pre-headless agent rows (no session_id)
  degrade to the context-re-injection fresh run
- settings_gen: permissions allowlist is always emitted
- credsync: change-triggered uploads key on .claude/.credentials.json
  only (claude touches ~/.claude.json every run - keying on it would
  ping-pong uploads between workers); logins still publish explicitly
- cli list: liveness from agent_runs in the DB, not tmux
- tests: spawn/kill/resume re-pointed at the fake_launch seam
  (conftest); integration test now drives API -> worker -> real fake
  claude subprocess -> events endpoint; README documents the headless
  model + multi-worker deployment invariants

Suite 295 green; frontend unchanged since phase 3.
2026-07-21 23:20:39 -04:00

104 lines
3.7 KiB
Python

"""Credential distribution through runtime_secrets: the login completes on one worker,
every other worker materializes the encrypted bundle from the DB — no shared files."""
from __future__ import annotations
import json
import os
from pathlib import Path
import pytest
from cryptography.fernet import Fernet
from handler.control import credsync
from handler.db import repository as repo
from handler.db.engine import get_engine
@pytest.fixture
def secret_env(env, monkeypatch):
from handler import config
monkeypatch.setenv("HANDLER_SECRET_KEY", Fernet.generate_key().decode())
config.get_settings.cache_clear()
credsync._state.__init__() # fresh sync cursor per test
yield env
config.get_settings.cache_clear()
def _write_local_credentials(home: Path) -> None:
(home / ".claude").mkdir(parents=True, exist_ok=True)
(home / ".claude.json").write_text(
json.dumps({"oauthAccount": {"email": "op@example.com"}, "theme": "light"})
)
(home / ".claude" / ".credentials.json").write_text('{"token": "secret-oauth-token"}')
def test_upload_stores_encrypted_bundle(secret_env, tmp_path):
_write_local_credentials(tmp_path)
assert credsync.upload() is True
with get_engine().begin() as conn:
row = repo.get_runtime_secret(conn, credsync.SECRET_KEY)
assert row is not None
# Ciphertext at rest — the raw token must not appear in the DB value.
assert "secret-oauth-token" not in row["value_enc"]
def test_refresh_materializes_on_fresh_worker(secret_env, tmp_path, monkeypatch):
_write_local_credentials(tmp_path)
assert credsync.upload() is True
other_home = tmp_path / "worker-b"
other_home.mkdir()
monkeypatch.setenv("HOME", str(other_home))
credsync._state.__init__() # worker B's process state
assert credsync.refresh() == "materialized"
creds = json.loads((other_home / ".claude" / ".credentials.json").read_text())
assert creds["token"] == "secret-oauth-token"
data = json.loads((other_home / ".claude.json").read_text())
assert data["oauthAccount"]["email"] == "op@example.com"
# A second pass is a no-op — nothing changed anywhere.
assert credsync.refresh() is None
def test_materialize_merges_claude_json_preserving_local_state(secret_env, tmp_path, monkeypatch):
_write_local_credentials(tmp_path)
credsync.upload()
other_home = tmp_path / "worker-c"
(other_home / ".claude").mkdir(parents=True)
(other_home / ".claude.json").write_text(
json.dumps(
{
"hasCompletedOnboarding": True,
"theme": "dark",
"projects": {"/projects/p/a": {"hasTrustDialogAccepted": True}},
}
)
)
monkeypatch.setenv("HOME", str(other_home))
credsync._state.__init__()
assert credsync.refresh() == "materialized"
data = json.loads((other_home / ".claude.json").read_text())
# Account arrived...
assert data["oauthAccount"]["email"] == "op@example.com"
# ...but this worker's own onboarding/trust state (claude_config's writes) survived.
assert data["theme"] == "dark"
assert data["projects"]["/projects/p/a"]["hasTrustDialogAccepted"] is True
def test_refresh_uploads_local_change(secret_env, tmp_path):
_write_local_credentials(tmp_path)
assert credsync.refresh() == "uploaded" # bootstrap: local creds, empty DB
# A token refresh on disk (mtime/size change) re-publishes.
os.utime(tmp_path / ".claude" / ".credentials.json", ns=(1, 1))
assert credsync.refresh() == "uploaded"
def test_disabled_without_secret_key(env, tmp_path):
_write_local_credentials(tmp_path)
assert credsync.upload() is False
assert credsync.refresh() is None