mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-06 17:47:14 +00:00
0f216a0d4d
* feat(provider): support Codex subscriptions with ChatGPT sign-in * style: format Codex live integration test * fix(provider): preserve Codex identity in temporary model tests * fix(web): portal provider selector without dialog overflow * fix(web): allow native scrolling in provider dropdown * fix(provider): surface safe Codex quota and upstream errors * fix(web): provide reliable Codex copy feedback in dialogs * feat(provider): confirm cascade deletion from edit dialog * fix(persistence): discard connections after failed commit * fix(web): polish provider loading and confirmation motion --------- Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
107 lines
5.4 KiB
Python
107 lines
5.4 KiB
Python
"""Exercise Codex provider wiring through a real LangBot process.
|
|
|
|
The default run does not contact OpenAI. Set LANGBOT_TEST_CODEX_DEVICE_AUTH=1
|
|
to also exercise live device start/pending/cancel, without account sign-in.
|
|
OAuth exchange and inference behavior are covered by deterministic tests.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import time
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.e2e
|
|
|
|
|
|
def test_codex_provider_disconnected_journey(e2e_client):
|
|
credentials = {'user': 'codex-e2e@example.com', 'password': 'codex-local-test-password'}
|
|
initialized = e2e_client.post('/api/v1/user/init', json=credentials)
|
|
assert initialized.status_code == 200, initialized.text
|
|
authenticated = e2e_client.post('/api/v1/user/auth', json=credentials)
|
|
assert authenticated.status_code == 200, authenticated.text
|
|
headers = {'Authorization': f'Bearer {authenticated.json()["data"]["token"]}'}
|
|
bootstrap = e2e_client.get('/api/v1/workspaces/bootstrap', headers=headers)
|
|
assert bootstrap.status_code == 200, bootstrap.text
|
|
headers['X-Workspace-Id'] = bootstrap.json()['data']['workspaces'][0]['workspace']['uuid']
|
|
|
|
requesters = e2e_client.get('/api/v1/provider/requesters?type=llm', headers=headers)
|
|
assert requesters.status_code == 200, requesters.text
|
|
codex = next(item for item in requesters.json()['data']['requesters'] if item['name'] == 'openai-codex')
|
|
assert codex['spec']['support_type'] == ['llm']
|
|
icon = e2e_client.get('/api/v1/provider/requesters/openai-codex/icon')
|
|
assert icon.status_code == 200
|
|
assert 'image/' in icon.headers['content-type']
|
|
|
|
base = '/api/v1/provider/providers'
|
|
created = e2e_client.post(
|
|
base,
|
|
headers=headers,
|
|
json={'name': 'Codex E2E', 'requester': 'openai-codex', 'base_url': '', 'api_keys': []},
|
|
)
|
|
assert created.status_code == 200, created.text
|
|
provider_path = f'{base}/{created.json()["data"]["uuid"]}'
|
|
try:
|
|
provider = e2e_client.get(provider_path, headers=headers)
|
|
assert provider.status_code == 200, provider.text
|
|
data = provider.json()['data']['provider']
|
|
assert data['requester'] == 'openai-codex'
|
|
assert data['api_keys'] == []
|
|
assert data['base_url'] == 'https://chatgpt.com/backend-api/codex'
|
|
assert not {'access_token', 'refresh_token', 'id_token'} & data.keys()
|
|
|
|
status = e2e_client.get(f'{provider_path}/codex/status', headers=headers)
|
|
assert status.status_code == 200, status.text
|
|
assert status.json()['data']['connected'] is False
|
|
assert status.json()['data']['status'] == 'disconnected'
|
|
|
|
anonymous = e2e_client.post(f'{provider_path}/codex/device', json={})
|
|
assert anonymous.status_code == 401
|
|
invalid = e2e_client.put(provider_path, headers=headers, json={'base_url': 'https://example.com'})
|
|
assert invalid.status_code == 400, invalid.text
|
|
invalid_key = e2e_client.put(provider_path, headers=headers, json={'api_keys': ['not-a-codex-key']})
|
|
assert invalid_key.status_code == 400, invalid_key.text
|
|
|
|
scanned = e2e_client.get(f'{provider_path}/scan-models?type=llm', headers=headers)
|
|
assert scanned.status_code == 400, scanned.text
|
|
assert 'sign in' in scanned.json()['msg'].lower()
|
|
|
|
renamed = e2e_client.put(provider_path, headers=headers, json={'name': 'Codex renamed'})
|
|
assert renamed.status_code == 200, renamed.text
|
|
reread = e2e_client.get(provider_path, headers=headers)
|
|
assert reread.json()['data']['provider']['name'] == 'Codex renamed'
|
|
disconnected = e2e_client.delete(f'{provider_path}/codex/auth', headers=headers)
|
|
assert disconnected.status_code == 200, disconnected.text
|
|
|
|
# Opt-in smoke contacts real OpenAI device endpoints, but never completes
|
|
# account sign-in or prints the one-time code/device credentials.
|
|
if os.environ.get('LANGBOT_TEST_CODEX_DEVICE_AUTH') == '1':
|
|
started = e2e_client.post(f'{provider_path}/codex/device', headers=headers, json={})
|
|
assert started.status_code == 200, started.json().get('msg', 'Device start failed')
|
|
attempt = started.json()['data']
|
|
assert attempt['verification_uri'] == 'https://auth.openai.com/codex/device'
|
|
assert isinstance(attempt['user_code'], str) and attempt['user_code']
|
|
assert 0 < attempt['expires_at'] - time.time() <= 900
|
|
assert not {'access_token', 'refresh_token', 'device_auth_id'} & attempt.keys()
|
|
time.sleep(attempt['interval'])
|
|
pending = e2e_client.post(
|
|
f'{provider_path}/codex/device/poll',
|
|
headers=headers,
|
|
json={'authorization_id': attempt['authorization_id']},
|
|
)
|
|
assert pending.status_code == 200
|
|
assert pending.json()['data']['status'] == 'pending'
|
|
canceled = e2e_client.delete(f'{provider_path}/codex/device/{attempt["authorization_id"]}', headers=headers)
|
|
assert canceled.status_code == 200
|
|
expired = e2e_client.post(
|
|
f'{provider_path}/codex/device/poll',
|
|
headers=headers,
|
|
json={'authorization_id': attempt['authorization_id']},
|
|
)
|
|
assert expired.json()['data']['status'] == 'expired'
|
|
finally:
|
|
deleted = e2e_client.delete(provider_path, headers=headers)
|
|
assert deleted.status_code == 200, deleted.text
|
|
assert e2e_client.get(provider_path, headers=headers).status_code == 404
|