fix Empty-string project/team IDs bug

This commit is contained in:
Swifty
2026-02-13 12:26:23 +01:00
parent 16061aefc6
commit c8f08b4659
2 changed files with 55 additions and 40 deletions

View File

@@ -98,18 +98,33 @@ def _get_settings() -> Settings:
return _settings
def _get_linear_client() -> LinearClient:
"""Create a Linear client using the system API key from settings."""
api_key = _get_settings().secrets.linear_api_key
if not api_key:
raise RuntimeError("LINEAR_API_KEY secret is not configured")
def _get_linear_config() -> tuple[LinearClient, str, str]:
"""Return a configured Linear client, project ID, and team ID.
Raises RuntimeError if any required setting is missing.
"""
secrets = _get_settings().secrets
if not secrets.linear_api_key:
raise RuntimeError("LINEAR_API_KEY is not configured")
if not secrets.linear_feature_request_project_id:
raise RuntimeError(
"LINEAR_FEATURE_REQUEST_PROJECT_ID is not configured"
)
if not secrets.linear_feature_request_team_id:
raise RuntimeError("LINEAR_FEATURE_REQUEST_TEAM_ID is not configured")
credentials = APIKeyCredentials(
id="system-linear",
provider="linear",
api_key=SecretStr(api_key),
api_key=SecretStr(secrets.linear_api_key),
title="System Linear API Key",
)
return LinearClient(credentials=credentials)
client = LinearClient(credentials=credentials)
return (
client,
secrets.linear_feature_request_project_id,
secrets.linear_feature_request_team_id,
)
class SearchFeatureRequestsTool(BaseTool):
@@ -161,16 +176,13 @@ class SearchFeatureRequestsTool(BaseTool):
)
try:
secrets = _get_settings().secrets
client = _get_linear_client()
client, project_id, _team_id = _get_linear_config()
data = await client.query(
SEARCH_ISSUES_QUERY,
{
"term": query,
"filter": {
"project": {
"id": {"eq": secrets.linear_feature_request_project_id}
},
"project": {"id": {"eq": project_id}},
},
"first": MAX_SEARCH_RESULTS,
},
@@ -310,10 +322,9 @@ class CreateFeatureRequestTool(BaseTool):
)
try:
secrets = _get_settings().secrets
client = _get_linear_client()
client, project_id, team_id = _get_linear_config()
except Exception as e:
logger.exception("Failed to create Linear client")
logger.exception("Failed to initialize Linear client")
return ErrorResponse(
message="Failed to create feature request.",
error=str(e),
@@ -358,8 +369,8 @@ class CreateFeatureRequestTool(BaseTool):
"input": {
"title": title,
"description": description,
"teamId": secrets.linear_feature_request_team_id,
"projectId": secrets.linear_feature_request_project_id,
"teamId": team_id,
"projectId": project_id,
},
},
)

View File

@@ -26,8 +26,12 @@ _TEST_USER_EMAIL = "testuser@example.com"
# ---------------------------------------------------------------------------
def _mock_linear_client(*, query_return=None, mutate_return=None):
"""Return a patched _get_linear_client that yields a mock LinearClient."""
_FAKE_PROJECT_ID = "test-project-id"
_FAKE_TEAM_ID = "test-team-id"
def _mock_linear_config(*, query_return=None, mutate_return=None):
"""Return a patched _get_linear_config that yields a mock LinearClient."""
client = AsyncMock()
if query_return is not None:
client.query.return_value = query_return
@@ -35,8 +39,8 @@ def _mock_linear_client(*, query_return=None, mutate_return=None):
client.mutate.return_value = mutate_return
return (
patch(
"backend.api.features.chat.tools.feature_requests._get_linear_client",
return_value=client,
"backend.api.features.chat.tools.feature_requests._get_linear_config",
return_value=(client, _FAKE_PROJECT_ID, _FAKE_TEAM_ID),
),
client,
)
@@ -126,7 +130,7 @@ class TestSearchFeatureRequestsTool:
"description": None,
},
]
patcher, _ = _mock_linear_client(query_return=_search_response(nodes))
patcher, _ = _mock_linear_config(query_return=_search_response(nodes))
with patcher:
tool = SearchFeatureRequestsTool()
resp = await tool._execute(
@@ -142,7 +146,7 @@ class TestSearchFeatureRequestsTool:
@pytest.mark.asyncio(loop_scope="session")
async def test_no_results(self):
session = make_session(user_id=_TEST_USER_ID)
patcher, _ = _mock_linear_client(query_return=_search_response([]))
patcher, _ = _mock_linear_config(query_return=_search_response([]))
with patcher:
tool = SearchFeatureRequestsTool()
resp = await tool._execute(
@@ -173,7 +177,7 @@ class TestSearchFeatureRequestsTool:
@pytest.mark.asyncio(loop_scope="session")
async def test_api_failure(self):
session = make_session(user_id=_TEST_USER_ID)
patcher, client = _mock_linear_client()
patcher, client = _mock_linear_config()
client.query.side_effect = RuntimeError("Linear API down")
with patcher:
tool = SearchFeatureRequestsTool()
@@ -191,7 +195,7 @@ class TestSearchFeatureRequestsTool:
session = make_session(user_id=_TEST_USER_ID)
# Node missing 'identifier' key
bad_nodes = [{"id": "id-1", "title": "Missing identifier"}]
patcher, _ = _mock_linear_client(query_return=_search_response(bad_nodes))
patcher, _ = _mock_linear_config(query_return=_search_response(bad_nodes))
with patcher:
tool = SearchFeatureRequestsTool()
resp = await tool._execute(
@@ -204,7 +208,7 @@ class TestSearchFeatureRequestsTool:
async def test_linear_client_init_failure(self):
session = make_session(user_id=_TEST_USER_ID)
with patch(
"backend.api.features.chat.tools.feature_requests._get_linear_client",
"backend.api.features.chat.tools.feature_requests._get_linear_config",
side_effect=RuntimeError("No API key"),
):
tool = SearchFeatureRequestsTool()
@@ -241,7 +245,7 @@ class TestCreateFeatureRequestTool:
"""Full happy path: upsert customer -> create issue -> attach need."""
session = make_session(user_id=_TEST_USER_ID)
patcher, client = _mock_linear_client()
patcher, client = _mock_linear_config()
client.mutate.side_effect = [
_customer_upsert_response(),
_issue_create_response(),
@@ -268,7 +272,7 @@ class TestCreateFeatureRequestTool:
"""When existing_issue_id is provided, skip issue creation."""
session = make_session(user_id=_TEST_USER_ID)
patcher, client = _mock_linear_client()
patcher, client = _mock_linear_config()
client.mutate.side_effect = [
_customer_upsert_response(),
_need_create_response(issue_id="existing-1", identifier="FR-99"),
@@ -343,7 +347,7 @@ class TestCreateFeatureRequestTool:
async def test_linear_client_init_failure(self):
session = make_session(user_id=_TEST_USER_ID)
with patch(
"backend.api.features.chat.tools.feature_requests._get_linear_client",
"backend.api.features.chat.tools.feature_requests._get_linear_config",
side_effect=RuntimeError("No API key"),
):
tool = CreateFeatureRequestTool()
@@ -363,7 +367,7 @@ class TestCreateFeatureRequestTool:
@pytest.mark.asyncio(loop_scope="session")
async def test_customer_upsert_api_error(self):
session = make_session(user_id=_TEST_USER_ID)
patcher, client = _mock_linear_client()
patcher, client = _mock_linear_config()
client.mutate.side_effect = RuntimeError("Customer API error")
with patcher:
@@ -382,7 +386,7 @@ class TestCreateFeatureRequestTool:
@pytest.mark.asyncio(loop_scope="session")
async def test_customer_upsert_not_success(self):
session = make_session(user_id=_TEST_USER_ID)
patcher, client = _mock_linear_client()
patcher, client = _mock_linear_config()
client.mutate.return_value = _customer_upsert_response(success=False)
with patcher:
@@ -400,7 +404,7 @@ class TestCreateFeatureRequestTool:
async def test_customer_malformed_response(self):
"""Customer dict missing 'id' key should be caught."""
session = make_session(user_id=_TEST_USER_ID)
patcher, client = _mock_linear_client()
patcher, client = _mock_linear_config()
# success=True but customer has no 'id'
client.mutate.return_value = {
"customerUpsert": {
@@ -425,7 +429,7 @@ class TestCreateFeatureRequestTool:
@pytest.mark.asyncio(loop_scope="session")
async def test_issue_create_api_error(self):
session = make_session(user_id=_TEST_USER_ID)
patcher, client = _mock_linear_client()
patcher, client = _mock_linear_config()
client.mutate.side_effect = [
_customer_upsert_response(),
RuntimeError("Issue create failed"),
@@ -447,7 +451,7 @@ class TestCreateFeatureRequestTool:
@pytest.mark.asyncio(loop_scope="session")
async def test_issue_create_not_success(self):
session = make_session(user_id=_TEST_USER_ID)
patcher, client = _mock_linear_client()
patcher, client = _mock_linear_config()
client.mutate.side_effect = [
_customer_upsert_response(),
_issue_create_response(success=False),
@@ -469,7 +473,7 @@ class TestCreateFeatureRequestTool:
async def test_issue_create_malformed_response(self):
"""issueCreate success=True but missing 'issue' key."""
session = make_session(user_id=_TEST_USER_ID)
patcher, client = _mock_linear_client()
patcher, client = _mock_linear_config()
client.mutate.side_effect = [
_customer_upsert_response(),
{"issueCreate": {"success": True}}, # no 'issue' key
@@ -492,7 +496,7 @@ class TestCreateFeatureRequestTool:
async def test_need_create_api_error_new_issue(self):
"""Need creation fails after new issue was created -> orphaned issue info."""
session = make_session(user_id=_TEST_USER_ID)
patcher, client = _mock_linear_client()
patcher, client = _mock_linear_config()
client.mutate.side_effect = [
_customer_upsert_response(),
_issue_create_response(issue_id="orphan-1", identifier="FR-10"),
@@ -519,7 +523,7 @@ class TestCreateFeatureRequestTool:
async def test_need_create_api_error_existing_issue(self):
"""Need creation fails on existing issue -> no orphaned info."""
session = make_session(user_id=_TEST_USER_ID)
patcher, client = _mock_linear_client()
patcher, client = _mock_linear_config()
client.mutate.side_effect = [
_customer_upsert_response(),
RuntimeError("Need attach failed"),
@@ -542,7 +546,7 @@ class TestCreateFeatureRequestTool:
async def test_need_create_not_success_includes_orphaned_info(self):
"""customerNeedCreate returns success=False -> includes orphaned issue."""
session = make_session(user_id=_TEST_USER_ID)
patcher, client = _mock_linear_client()
patcher, client = _mock_linear_config()
client.mutate.side_effect = [
_customer_upsert_response(),
_issue_create_response(issue_id="orphan-2", identifier="FR-20"),
@@ -567,7 +571,7 @@ class TestCreateFeatureRequestTool:
async def test_need_create_not_success_existing_issue_no_details(self):
"""customerNeedCreate fails on existing issue -> no orphaned info."""
session = make_session(user_id=_TEST_USER_ID)
patcher, client = _mock_linear_client()
patcher, client = _mock_linear_config()
client.mutate.side_effect = [
_customer_upsert_response(),
_need_create_response(success=False),
@@ -590,7 +594,7 @@ class TestCreateFeatureRequestTool:
async def test_need_create_malformed_response(self):
"""need_result missing 'need' key after success=True."""
session = make_session(user_id=_TEST_USER_ID)
patcher, client = _mock_linear_client()
patcher, client = _mock_linear_config()
client.mutate.side_effect = [
_customer_upsert_response(),
_issue_create_response(),