mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
231 lines
8.3 KiB
Python
231 lines
8.3 KiB
Python
"""A cloud workflow may never exist without a credential the cloud can sign it with.
|
|
|
|
Before this wiring, `lease_to_cloud` had zero callers outside its own unit tests. Every part
|
|
worked and nothing joined them, so every cloud run in existence died at dispatch with
|
|
`no_cloud_credential` and the user found out at 9am. These tests pin the join.
|
|
"""
|
|
import pytest
|
|
|
|
from backend.apps.nine_router.credential_lease import LeaseOutcome
|
|
from backend.apps.workflows.cloud import credential_readiness, handover
|
|
|
|
|
|
@pytest.fixture
|
|
def p_oauth(monkeypatch):
|
|
def set_ids(ids):
|
|
monkeypatch.setattr(
|
|
credential_readiness.credential_store, "list_oauth_connection_ids", lambda: list(ids)
|
|
)
|
|
monkeypatch.setattr(
|
|
handover.credential_store, "list_oauth_connection_ids", lambda: list(ids)
|
|
)
|
|
return set_ids
|
|
|
|
|
|
@pytest.fixture
|
|
def p_lease(monkeypatch):
|
|
calls = []
|
|
|
|
def set_result(*statuses):
|
|
seq = list(statuses)
|
|
|
|
async def fake(connection_id: str) -> LeaseOutcome:
|
|
calls.append(connection_id)
|
|
return LeaseOutcome(status=seq.pop(0) if seq else "cloud_rejected")
|
|
|
|
monkeypatch.setattr(handover.credential_lease, "lease_to_cloud", fake)
|
|
return calls
|
|
|
|
return set_result
|
|
|
|
|
|
def test_an_account_with_only_api_keys_cannot_use_cloud_runs(p_oauth):
|
|
# Gemini AI Studio and a raw OpenAI key are apikey rows: no refresh token, nothing to lend.
|
|
p_oauth([])
|
|
r = credential_readiness.cloud_credential_readiness()
|
|
assert r.ok is False
|
|
assert r.state == "none_eligible"
|
|
assert "Claude or ChatGPT" in (r.reason or ""), "must name what to connect, not just refuse"
|
|
assert "API key" in (r.reason or ""), "the API-key user needs to know why theirs will not do"
|
|
|
|
|
|
def test_an_oauth_connection_reads_as_ready(p_oauth):
|
|
p_oauth(["conn-claude"])
|
|
r = credential_readiness.cloud_credential_readiness()
|
|
assert r.ok is True
|
|
assert r.connection_ids == ["conn-claude"]
|
|
assert r.reason is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lending_succeeds_on_the_first_usable_connection(p_oauth, p_lease):
|
|
p_oauth(["conn-a", "conn-b"])
|
|
calls = p_lease("leased")
|
|
out = await handover.lend_credential_for_cloud()
|
|
assert out.ok is True
|
|
assert calls == ["conn-a"], "one lease covers every cloud workflow; do not lend them all"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_already_stripped_connection_counts_as_lent(p_oauth, p_lease):
|
|
# not_rotatable means the local refresh token is already gone, i.e. the cloud has it.
|
|
p_oauth(["conn-a"])
|
|
p_lease("not_rotatable")
|
|
assert (await handover.lend_credential_for_cloud()).ok is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_refused_connection_falls_through_to_the_next(p_oauth, p_lease):
|
|
p_oauth(["conn-dead", "conn-good"])
|
|
calls = p_lease("cloud_rejected", "leased")
|
|
out = await handover.lend_credential_for_cloud()
|
|
assert out.ok is True
|
|
assert calls == ["conn-dead", "conn-good"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_every_connection_failing_refuses_with_words_a_user_can_act_on(p_oauth, p_lease):
|
|
p_oauth(["conn-a"])
|
|
p_lease("cloud_rejected")
|
|
out = await handover.lend_credential_for_cloud()
|
|
assert out.ok is False
|
|
assert "still runs on this device" in (out.message or ""), "say what DID happen, not just what failed"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_signed_out_says_sign_in_rather_than_a_lease_error(p_oauth, p_lease):
|
|
p_oauth(["conn-a"])
|
|
p_lease("not_signed_in")
|
|
out = await handover.lend_credential_for_cloud()
|
|
assert out.ok is False
|
|
assert out.message == handover.SIGN_IN_MESSAGE
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_unknown_lease_outcome_tells_the_user_to_reconnect(p_oauth, p_lease):
|
|
# The token is off this device and we cannot prove the cloud took it. Silence here strands them.
|
|
p_oauth(["conn-a"])
|
|
p_lease("ownership_unknown")
|
|
out = await handover.lend_credential_for_cloud()
|
|
assert out.ok is False
|
|
assert "reconnect" in (out.message or "").lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_eligible_connection_refuses_before_anything_is_lent(p_oauth, p_lease):
|
|
p_oauth([])
|
|
calls = p_lease("leased")
|
|
out = await handover.lend_credential_for_cloud()
|
|
assert out.ok is False
|
|
assert calls == [], "nothing to lend, so nothing should have been attempted"
|
|
assert "Claude or ChatGPT" in (out.message or "")
|
|
|
|
|
|
# The join itself. Everything above passes even if hand_to_cloud never calls any of it, which is
|
|
# exactly the shape of the bug being fixed: the parts all worked and nothing wired them together.
|
|
|
|
from backend.apps.workflows import storage
|
|
from backend.apps.workflows.cloud import client as cloud
|
|
from backend.apps.workflows.models import ScheduleConfig, Workflow, WorkflowStep
|
|
|
|
pytestmark = pytest.mark.usefixtures("isolated_workflows_data")
|
|
|
|
|
|
def p_wf() -> Workflow:
|
|
wf = Workflow(
|
|
title="Morning digest",
|
|
steps=[WorkflowStep(text="summarize the news")],
|
|
schedule=ScheduleConfig(
|
|
enabled=True, repeat_unit="day", repeat_every=1, hour=9, minute=0, timezone="UTC"
|
|
),
|
|
)
|
|
storage.save_workflow(wf)
|
|
return wf
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_workflow_never_reaches_the_cloud_without_a_credential(monkeypatch, p_oauth):
|
|
"""The 9am bug, pinned: no lendable account means the push must not happen at all."""
|
|
p_oauth([])
|
|
wf = p_wf()
|
|
talked: list = []
|
|
|
|
async def p_call(method, path, body=None):
|
|
talked.append(path)
|
|
raise AssertionError("pushed a workflow the cloud could never run")
|
|
|
|
monkeypatch.setattr(cloud, "p_call", p_call)
|
|
|
|
out = await handover.hand_to_cloud(wf, enabled=True)
|
|
assert out.ok is False
|
|
assert talked == [], "the credential check has to come BEFORE the push"
|
|
assert wf.execution_target == "device", "a refused handover leaves it running here"
|
|
assert "Claude or ChatGPT" in (out.message or "")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_refused_push_gives_the_account_back(monkeypatch, p_oauth, p_lease):
|
|
"""A hobby user clicking Cloud leases fine (leasing does not check plan) and is then refused by
|
|
the server. Without giving it back, their account stays lent for a workflow that never went up
|
|
and this device can no longer refresh its own token."""
|
|
p_oauth(["conn-a"])
|
|
p_lease("leased")
|
|
wf = p_wf()
|
|
reclaimed: list = []
|
|
|
|
async def p_call(method, path, body=None):
|
|
raise cloud.CloudRefused("Cloud workflows need a Pro plan or higher.", 402)
|
|
|
|
async def fake_release(connection_id: str):
|
|
reclaimed.append(connection_id)
|
|
return LeaseOutcome(status="released")
|
|
|
|
monkeypatch.setattr(cloud, "p_call", p_call)
|
|
monkeypatch.setattr(handover.credential_lease, "release_to_device", fake_release)
|
|
|
|
out = await handover.hand_to_cloud(wf, enabled=True)
|
|
assert out.ok is False
|
|
assert out.message == "Cloud workflows need a Pro plan or higher."
|
|
assert reclaimed == ["conn-a"], "the account must come home when the workflow never went up"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_reclaim_spares_an_account_another_cloud_workflow_still_needs(monkeypatch, p_oauth, p_lease):
|
|
p_oauth(["conn-a"])
|
|
p_lease("leased")
|
|
keeper = p_wf()
|
|
keeper.execution_target = "cloud"
|
|
storage.save_workflow(keeper)
|
|
wf = p_wf()
|
|
reclaimed: list = []
|
|
|
|
async def p_call(method, path, body=None):
|
|
raise cloud.CloudRefused("nope", 402)
|
|
|
|
async def fake_release(connection_id: str):
|
|
reclaimed.append(connection_id)
|
|
return LeaseOutcome(status="released")
|
|
|
|
monkeypatch.setattr(cloud, "p_call", p_call)
|
|
monkeypatch.setattr(handover.credential_lease, "release_to_device", fake_release)
|
|
|
|
await handover.hand_to_cloud(wf, enabled=True)
|
|
assert reclaimed == [], "another cloud workflow still needs it; taking it back would break that one"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_failed_lease_leaves_the_workflow_on_this_device(monkeypatch, p_oauth, p_lease):
|
|
p_oauth(["conn-a"])
|
|
p_lease("cloud_rejected")
|
|
wf = p_wf()
|
|
|
|
async def p_call(method, path, body=None):
|
|
raise AssertionError("pushed despite the lease failing")
|
|
|
|
monkeypatch.setattr(cloud, "p_call", p_call)
|
|
|
|
out = await handover.hand_to_cloud(wf, enabled=True)
|
|
assert out.ok is False
|
|
assert wf.execution_target == "device"
|
|
assert storage.get_workflow(wf.id).execution_target == "device", "and it stayed that way on disk"
|