feat(api): expose task status to api keys

This commit is contained in:
Tynwink2000
2026-09-08 14:06:13 +08:00
parent 1fa5e2f755
commit 1a69747a06
4 changed files with 270 additions and 16 deletions
+82
View File
@@ -509,6 +509,11 @@ async def test_api_key_context_returns_bound_identity_without_workspace_permissi
'pipeline.update',
'pipeline.delete',
'pipeline.copy',
'task.list',
'task.get',
'knowledge_base.get',
'knowledge_base.file.store',
'file.document.upload',
]
)
assert all(item == {'supported': True} for item in capabilities['operations'].values())
@@ -556,6 +561,83 @@ async def test_api_key_context_returns_bound_identity_without_workspace_permissi
assert revoked_capabilities.status_code == 401
async def test_api_key_can_query_tasks_with_public_contract_and_resource_permission(workspace_api):
application, client, _, owner_token = workspace_api
task_query = {}
task_lookup = {}
fake_task = SimpleNamespace(
to_public_dict=lambda: {'id': 7, 'status': 'running', 'error': None, 'result': None},
to_dict=lambda: {'id': 7, 'runtime': {'state': 'PENDING'}},
)
def get_tasks_dict(*args, **kwargs):
task_query.update(kwargs)
if kwargs.get('public'):
return {'tasks': []}
return {'tasks': [], 'id_index': 1}
def get_task_by_id(*args, **kwargs):
task_lookup.update(kwargs)
return fake_task if args and args[0] == 7 else None
application.task_mgr = SimpleNamespace(
get_tasks_dict=get_tasks_dict,
get_task_by_id=get_task_by_id,
)
current_response = await client.get('/api/v1/workspaces/current', headers=_auth(owner_token))
workspace_uuid = (await current_response.get_json())['data']['workspace']['uuid']
create_response = await client.post(
'/api/v1/apikeys',
headers=_auth(owner_token, workspace_uuid),
json={'name': 'Task reader', 'scopes': ['resource.view']},
)
assert create_response.status_code == 200
key = (await create_response.get_json())['data']['key']['key']
listing = await client.get('/api/v1/system/tasks', headers={'X-API-Key': key})
assert listing.status_code == 200
assert (await listing.get_json())['data'] == {'tasks': []}
assert task_query['instance_uuid'] == application.workspace_service.instance_uuid
assert task_query['workspace_uuid'] == workspace_uuid
assert task_query['placement_generation'] == 1
assert task_query['public'] is True
bearer_listing = await client.get('/api/v1/system/tasks', headers=_auth(owner_token, workspace_uuid))
assert bearer_listing.status_code == 200
assert (await bearer_listing.get_json())['data'] == {'tasks': [], 'id_index': 1}
public_task = await client.get('/api/v1/system/tasks/7', headers={'X-API-Key': key})
assert public_task.status_code == 200
assert (await public_task.get_json())['data'] == {
'id': 7,
'status': 'running',
'error': None,
'result': None,
}
assert task_lookup == {
'instance_uuid': application.workspace_service.instance_uuid,
'workspace_uuid': workspace_uuid,
'placement_generation': 1,
}
legacy_task = await client.get('/api/v1/system/tasks/7', headers=_auth(owner_token, workspace_uuid))
assert legacy_task.status_code == 200
assert (await legacy_task.get_json())['data'] == {'id': 7, 'runtime': {'state': 'PENDING'}}
missing = await client.get('/api/v1/system/tasks/not-an-id', headers={'X-API-Key': key})
assert missing.status_code == 404
no_permission_response = await client.post(
'/api/v1/apikeys',
headers=_auth(owner_token, workspace_uuid),
json={'name': 'Task denied', 'scopes': []},
)
assert no_permission_response.status_code == 200
no_permission_key = (await no_permission_response.get_json())['data']['key']['key']
denied = await client.get('/api/v1/system/tasks', headers={'X-API-Key': no_permission_key})
assert denied.status_code == 403
async def test_cloud_projection_is_selected_explicitly_and_collaboration_runs_in_core(
workspace_api,
):
+123
View File
@@ -338,6 +338,69 @@ class TestTaskWrapper:
assert result['runtime']['exception'] == 'Test error'
assert 'exception_traceback' in result['runtime']
@pytest.mark.asyncio
async def test_public_dict_has_stable_success_projection(self):
_, TaskWrapper, _ = get_taskmgr_classes()
mock_app = create_mock_app()
async def successful_coro():
return {'file_id': 'file-a'}
wrapper = TaskWrapper(mock_app, successful_coro(), kind='knowledge_base.store')
await wrapper.task
result = wrapper.to_public_dict()
assert result == {
'id': wrapper.id,
'task_type': 'system',
'kind': 'knowledge_base.store',
'status': 'succeeded',
'error': None,
'result': {'file_id': 'file-a'},
'created_at': result['created_at'],
}
assert 'runtime' not in result
assert 'traceback' not in str(result).lower()
@pytest.mark.asyncio
async def test_public_dict_hides_exception_traceback(self):
_, TaskWrapper, _ = get_taskmgr_classes()
mock_app = create_mock_app()
async def failing_coro():
raise ValueError('private failure')
wrapper = TaskWrapper(mock_app, failing_coro())
try:
await wrapper.task
except ValueError:
pass
result = wrapper.to_public_dict()
assert result['status'] == 'failed'
assert result['error'] == {'type': 'task_failed', 'message': 'Task execution failed'}
assert 'runtime' not in result
assert 'traceback' not in str(result).lower()
@pytest.mark.asyncio
async def test_public_dict_does_not_change_success_when_result_is_not_json_serializable(self):
_, TaskWrapper, _ = get_taskmgr_classes()
mock_app = create_mock_app()
async def successful_coro():
return object()
wrapper = TaskWrapper(mock_app, successful_coro())
await wrapper.task
result = wrapper.to_public_dict()
assert result['status'] == 'succeeded'
assert result['error'] is None
assert result['result'] is None
@pytest.mark.asyncio
async def test_cancel_task(self):
"""Test cancel method cancels the asyncio task."""
@@ -487,6 +550,66 @@ class TestAsyncTaskManager:
w2.cancel()
w3.cancel()
@pytest.mark.asyncio
async def test_public_task_queries_keep_workspace_and_generation_isolation(self):
_, _, AsyncTaskManager = get_taskmgr_classes()
mock_app = create_mock_app()
manager = AsyncTaskManager(mock_app)
async def dummy_coro():
await asyncio.sleep(10)
current = manager.create_user_task(
dummy_coro(),
instance_uuid='instance-a',
workspace_uuid='workspace-a',
placement_generation=2,
)
other_workspace = manager.create_user_task(
dummy_coro(),
instance_uuid='instance-a',
workspace_uuid='workspace-b',
placement_generation=2,
)
stale_generation = manager.create_user_task(
dummy_coro(),
instance_uuid='instance-a',
workspace_uuid='workspace-a',
placement_generation=1,
)
result = manager.get_tasks_dict(
instance_uuid='instance-a',
workspace_uuid='workspace-a',
placement_generation=2,
public=True,
)
assert [task['id'] for task in result['tasks']] == [current.id]
assert 'id_index' not in result
assert (
manager.get_task_by_id(
other_workspace.id,
instance_uuid='instance-a',
workspace_uuid='workspace-a',
placement_generation=2,
)
is None
)
assert (
manager.get_task_by_id(
stale_generation.id,
instance_uuid='instance-a',
workspace_uuid='workspace-a',
placement_generation=2,
)
is None
)
current.cancel()
other_workspace.cancel()
stale_generation.cancel()
@pytest.mark.asyncio
async def test_cancel_by_scope(self):
"""Test cancel_by_scope cancels matching tasks."""