From 5dcfdf657da18c7f2754a2f4ac0af0a9cd6380bd Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Tue, 1 Sep 2026 01:27:34 -0500 Subject: [PATCH] fix(graphs): return the extension display name instead of dropping it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `getAvailableExtensions` built a display_names table, looked each extension up in it, put the result in a dict — and then discarded it when constructing the AvailableExtension response objects, which only carry name, description and enabled. So the only human-readable label the endpoint has never left it. Clients render `name`, which is the schema slug the create path takes, and the graph creation wizard headings its extension cards "roboledger" and "roboinvestor" is the visible consequence. Add display_name to the model and populate it on both paths. The fallback branch never had one at all, so hoist the table to module scope next to the descriptions it belongs with, where the same comment already explains why the constants are single-sourced. The field is optional so an older client is unaffected, and clients should fall back to the slug when it is absent. --- robosystems/models/api/entity_graph.py | 4 +++ robosystems/routers/graphs/main.py | 25 +++++++++++-------- .../routers/graphs/test_schema_integration.py | 13 ++++++++++ 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/robosystems/models/api/entity_graph.py b/robosystems/models/api/entity_graph.py index c2ef5acaf..9072bf56d 100644 --- a/robosystems/models/api/entity_graph.py +++ b/robosystems/models/api/entity_graph.py @@ -121,6 +121,10 @@ class EntityWithGraphResponse(BaseModel): class AvailableExtension(BaseModel): name: str + # The name to show a user. `name` is the schema slug the create path takes + # ("roboledger"), which is not what a tier or extension picker should be + # rendering — clients fall back to the slug only when this is absent. + display_name: str | None = None description: str enabled: bool = False diff --git a/robosystems/routers/graphs/main.py b/robosystems/routers/graphs/main.py index 519fb7fa7..7dff03e3c 100644 --- a/robosystems/routers/graphs/main.py +++ b/robosystems/routers/graphs/main.py @@ -54,8 +54,14 @@ router = APIRouter(prefix="/v1/graphs", tags=["Graphs"]) # Surfaced to prospects at graph-creation time, so these describe only what is -# built. Both are read twice below — the schema-loading path and its fallback — -# and single-sourcing them here is what keeps the two copies from diverging. +# built. All three are read twice below — the schema-loading path and its +# fallback — and single-sourcing them here is what keeps the copies from +# diverging. +_DISPLAY_NAMES = { + "roboledger": "RoboLedger - Accounting & Financial Reporting", + "roboinvestor": "RoboInvestor - Investment Management", +} + _ROBOLEDGER_DESCRIPTION = ( "Complete accounting system with XBRL reporting and GL transactions. " "Context-aware: SEC repositories get reporting-only tables, " @@ -538,12 +544,6 @@ async def get_available_extensions( f"Extension {ext_info['name']}: available={ext_info.get('available', False)}" ) if ext_info["available"]: - # Get display names for extensions - display_names = { - "roboledger": "RoboLedger - Accounting & Financial Reporting", - "roboinvestor": "RoboInvestor - Investment Management", - } - # Try to get actual node/relationship counts try: from robosystems.schemas.loader import ( @@ -577,7 +577,7 @@ async def get_available_extensions( available_extensions.append( { "name": ext_info["name"], - "display_name": display_names.get( + "display_name": _DISPLAY_NAMES.get( ext_info["name"], ext_info["name"].title() ), "description": description, # Use the correctly set description @@ -589,7 +589,10 @@ async def get_available_extensions( # Convert dictionaries to AvailableExtension objects extension_objects = [ AvailableExtension( - name=str(ext["name"]), description=str(ext["description"]), enabled=False + name=str(ext["name"]), + display_name=str(ext["display_name"]), + description=str(ext["description"]), + enabled=False, ) for ext in available_extensions ] @@ -605,11 +608,13 @@ async def get_available_extensions( extensions=[ AvailableExtension( name="roboledger", + display_name=_DISPLAY_NAMES["roboledger"], description=_ROBOLEDGER_DESCRIPTION, enabled=False, ), AvailableExtension( name="roboinvestor", + display_name=_DISPLAY_NAMES["roboinvestor"], description=_ROBOINVESTOR_DESCRIPTION, enabled=False, ), diff --git a/tests/routers/graphs/test_schema_integration.py b/tests/routers/graphs/test_schema_integration.py index b0c3b974d..35b62f732 100644 --- a/tests/routers/graphs/test_schema_integration.py +++ b/tests/routers/graphs/test_schema_integration.py @@ -360,6 +360,19 @@ async def test_schema_compatibility_workflow( extensions = extensions_response.json() assert len(extensions["extensions"]) == 2 + # `name` is the slug the create path takes; `display_name` is what a + # picker renders. The display name used to be computed and then dropped + # when the response model was built, leaving clients with "roboledger". + by_name = {ext["name"]: ext for ext in extensions["extensions"]} + assert ( + by_name["roboledger"]["display_name"] + == "RoboLedger - Accounting & Financial Reporting" + ) + assert ( + by_name["roboinvestor"]["display_name"] + == "RoboInvestor - Investment Management" + ) + # Now create a schema that should be compatible with roboledger financial_schema = { "name": "custom_financial",