Files
openworker/tests/test_accounts.py
T
Rohit C PrasadandDevika 2b45018ffa OpenWorker: initial import
Imported from andrewyng/aisuite@1b4bbf303e
(contents of its platform/ directory, hoisted to the repo root).
Development history prior to this commit lives in that repository.

Co-authored-by: Devika <devikaverma11@gmail.com>
2026-07-21 11:09:41 -07:00

178 lines
7.4 KiB
Python

"""Generic multi-account layer (accounts.py): the one implementation behind
every batch-2 connector's multiple accounts — per-account profiles at
`<connector>:account:<id>`, pointer-only `:default`, lazy legacy migration.
Mirrors the shipped gmail/gcal semantics, parameterized by connector."""
from __future__ import annotations
import pytest
from coworker.connectors import accounts, descriptors
from coworker.connectors.descriptors import ConnectorDescriptor, Field, ValidationResult
from coworker.connectors.setup import (
connect_connector,
connector_list,
disconnect_connector,
)
from coworker.secrets import SecretStore
def _fake_descriptor(name="acmeapp", account_field="project_id", managed=False):
return ConnectorDescriptor(
name=name,
title="AcmeApp",
icon="◇",
blurb="test connector",
auth="api_token",
two_way=False,
fields=[
Field("api_key", "API key", secret=True),
Field("project_id", "Project ID", secret=False),
],
instructions=[],
validate=lambda creds: ValidationResult(True, identity="acme@example.com"),
managed=managed,
account_field=account_field,
)
@pytest.fixture
def acme():
"""A registered account-patterned test connector, removed afterwards."""
d = _fake_descriptor()
descriptors.register_descriptor(d)
yield d
descriptors.DESCRIPTORS.remove(d)
descriptors._BY_NAME.pop(d.name, None)
@pytest.fixture
def secrets(tmp_path):
return SecretStore(tmp_path / "secrets.json")
def test_add_list_resolve_default(acme, secrets):
assert accounts.add_account(secrets, "acmeapp", "p1", {"api_key": "k1"})["ok"]
accounts.add_account(secrets, "acmeapp", "p2", {"api_key": "k2"})
assert [a for a, _ in accounts.list_accounts(secrets, "acmeapp")] == ["p1", "p2"]
assert accounts.default_account(secrets, "acmeapp") == "p1" # first stays default
# resolve: explicit, default fallback, unknown
aid, key, profile = accounts.resolve(secrets, "acmeapp", "p2")
assert (aid, key, profile["api_key"]) == ("p2", "acmeapp:account:p2", "k2")
aid, _, profile = accounts.resolve(secrets, "acmeapp")
assert aid == "p1" and profile["api_key"] == "k1"
assert accounts.resolve(secrets, "acmeapp", "nope")[2] is None
# the default pointer profile never carries credentials
assert "api_key" not in (secrets.get("acmeapp:default") or {})
assert accounts.set_default(secrets, "acmeapp", "p2")["ok"]
assert accounts.default_account(secrets, "acmeapp") == "p2"
assert not accounts.set_default(secrets, "acmeapp", "ghost")["ok"]
def test_disconnect_moves_pointer_then_deletes_it(acme, secrets):
accounts.add_account(secrets, "acmeapp", "p1", {"api_key": "k1"})
accounts.add_account(secrets, "acmeapp", "p2", {"api_key": "k2"})
assert accounts.disconnect_account(secrets, "acmeapp", "p1")["ok"]
assert accounts.default_account(secrets, "acmeapp") == "p2" # pointer moved
out = accounts.disconnect_account(secrets, "acmeapp", "p2")
assert out["ok"] and out["remaining_accounts"] == 0
assert secrets.get("acmeapp:default") is None # pointer gone with last account
def test_legacy_default_migrates_lazily(acme, secrets):
"""A credential-bearing :default from an older build becomes one account on
first touch — same no-user-action migration gmail/gcal shipped."""
secrets.put(
"acmeapp:default",
{"type": "token", "enabled": True, "api_key": "old", "project_id": "42"},
)
assert [a for a, _ in accounts.list_accounts(secrets, "acmeapp")] == ["42"]
_, _, profile = accounts.resolve(secrets, "acmeapp")
assert profile["api_key"] == "old"
default = secrets.get("acmeapp:default")
assert default["default_account"] == "42" and "api_key" not in default
def test_connect_connector_adds_accounts_not_overwrites(acme, secrets):
"""The manual connect path: two submits with different account ids = two
accounts; same id = credential update in place."""
out = connect_connector(
secrets, "acmeapp", {"api_key": "k1", "project_id": "11"}, validate=False
)
assert out["ok"] and out["account_id"] == "11"
connect_connector(
secrets, "acmeapp", {"api_key": "k2", "project_id": "22"}, validate=False
)
connect_connector(
secrets, "acmeapp", {"api_key": "k2b", "project_id": "22"}, validate=False
)
ids = [a for a, _ in accounts.list_accounts(secrets, "acmeapp")]
assert ids == ["11", "22"]
assert accounts.resolve(secrets, "acmeapp", "22")[2]["api_key"] == "k2b"
assert accounts.default_account(secrets, "acmeapp") == "11"
def test_identity_sentinel_uses_validator_identity(secrets):
d = _fake_descriptor(name="acmemail", account_field=accounts.IDENTITY)
d.fields = [Field("api_key", "API key", secret=True)]
descriptors.register_descriptor(d)
try:
out = connect_connector(secrets, "acmemail", {"api_key": "k"}) # validate=True
assert out["ok"] and out["account_id"] == "acme@example.com"
finally:
descriptors.DESCRIPTORS.remove(d)
descriptors._BY_NAME.pop(d.name, None)
def test_connector_list_accounts_branch_and_full_disconnect(acme, secrets):
accounts.add_account(
secrets, "acmeapp", "p1", {"api_key": "k1", "account": "Proj One"}
)
accounts.add_account(secrets, "acmeapp", "p2", {"api_key": "k2", "managed": True})
entry = next(c for c in connector_list(secrets) if c["name"] == "acmeapp")
assert entry["connected"] and entry["enabled"]
assert entry["accounts"] == [
{"account_id": "p1", "name": "Proj One", "default": True, "managed": False},
{"account_id": "p2", "name": "p2", "default": False, "managed": True},
]
assert entry["account"] == "Proj One" # default account's display name
assert disconnect_connector(secrets, "acmeapp")["ok"]
assert accounts.list_accounts(secrets, "acmeapp") == []
entry = next(c for c in connector_list(secrets) if c["name"] == "acmeapp")
assert not entry["connected"]
def test_generic_account_routes(acme, secrets, tmp_path, monkeypatch):
"""The generic /accounts/{id}/disconnect|default routes work for
account-patterned connectors and refuse everything else."""
from fastapi.testclient import TestClient
from coworker.providers import ModelCapabilities, ProviderClient
from coworker.server.app import create_app
from coworker.server.manager import SessionManager
class _Provider(ProviderClient):
def complete(self, *, model, messages, tools=None, **settings):
raise AssertionError("no turns expected")
def capabilities(self, model):
return ModelCapabilities()
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
manager = SessionManager(workspace=tmp_path, provider=_Provider())
accounts.add_account(manager.secrets, "acmeapp", "p1", {"api_key": "k1"})
accounts.add_account(manager.secrets, "acmeapp", "p2", {"api_key": "k2"})
client = TestClient(create_app(manager))
assert client.post("/v1/connectors/acmeapp/accounts/p2/default").json()["ok"]
assert accounts.default_account(manager.secrets, "acmeapp") == "p2"
assert client.post("/v1/connectors/acmeapp/accounts/p1/disconnect").json()["ok"]
assert [a for a, _ in accounts.list_accounts(manager.secrets, "acmeapp")] == ["p2"]
out = client.post("/v1/connectors/linear/accounts/x/default").json()
assert not out["ok"] and "not a multi-account" in out["error"]