From c5c4b20cfe4785904282c6845a8183a1c9a473f2 Mon Sep 17 00:00:00 2001 From: potter Date: Thu, 14 May 2026 18:22:25 +0800 Subject: [PATCH] Make member lifecycle context strict --- .../bind/StudioMemberBindPanel.test.tsx | 14 +- .../components/bind/StudioMemberBindPanel.tsx | 139 ++++++++++-------- .../src/pages/studio/index.test.tsx | 72 +++++++++ .../src/pages/studio/index.tsx | 9 +- 4 files changed, 168 insertions(+), 66 deletions(-) diff --git a/apps/aevatar-console-web/src/pages/studio/components/bind/StudioMemberBindPanel.test.tsx b/apps/aevatar-console-web/src/pages/studio/components/bind/StudioMemberBindPanel.test.tsx index e4b53cf40..ec7893118 100644 --- a/apps/aevatar-console-web/src/pages/studio/components/bind/StudioMemberBindPanel.test.tsx +++ b/apps/aevatar-console-web/src/pages/studio/components/bind/StudioMemberBindPanel.test.tsx @@ -284,9 +284,17 @@ describe('StudioMemberBindPanel', () => { expect(screen.getByTestId('studio-bind-smoke-test-section')).toBeTruthy(); expect(screen.getByTestId('studio-bind-snippet-section')).toBeTruthy(); expect(screen.getByTestId('studio-bind-supporting-section')).toBeTruthy(); - fireEvent.click(screen.getByText('Published contract source')); - expect(await screen.findByText('Published service')).toBeTruthy(); - expect(primaryGrid.contains(screen.getByText('Published service'))).toBe(false); + expect(screen.getByText('Current member publication')).toBeTruthy(); + expect(screen.getByText('member:default')).toBeTruthy(); + expect(screen.queryByRole('combobox')).toBeNull(); + expect( + screen.queryByText('Select a published service'), + ).toBeNull(); + expect(screen.getByRole('button', { name: 'Chat' })).toHaveAttribute( + 'aria-pressed', + 'true', + ); + expect(screen.queryByText('Published service')).toBeNull(); expect(screen.queryByText('Binding Contract')).toBeNull(); expect(screen.queryByText('Current contract')).toBeNull(); expect(screen.queryByText('Published contract context')).toBeNull(); diff --git a/apps/aevatar-console-web/src/pages/studio/components/bind/StudioMemberBindPanel.tsx b/apps/aevatar-console-web/src/pages/studio/components/bind/StudioMemberBindPanel.tsx index bf4aa46f0..a36ef3316 100644 --- a/apps/aevatar-console-web/src/pages/studio/components/bind/StudioMemberBindPanel.tsx +++ b/apps/aevatar-console-web/src/pages/studio/components/bind/StudioMemberBindPanel.tsx @@ -5,7 +5,7 @@ import { LinkOutlined, } from '@ant-design/icons'; import { useQuery } from '@tanstack/react-query'; -import { Alert, Button, Collapse, Empty, Input, Select, Space, Tag, Typography, message } from 'antd'; +import { Alert, Button, Collapse, Empty, Input, Space, Tag, Typography, message } from 'antd'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { applyRuntimeEvent, @@ -249,9 +249,31 @@ const sourceControlStackStyle: React.CSSProperties = { minWidth: 0, }; -const sourceControlSelectStyle: React.CSSProperties = { - height: 58, - width: '100%', +const endpointChoiceRowStyle: React.CSSProperties = { + display: 'flex', + flexWrap: 'wrap', + gap: 6, +}; + +const endpointChoiceButtonStyle: React.CSSProperties = { + alignItems: 'center', + background: '#ffffff', + border: '1px solid #d9e2ef', + borderRadius: 999, + color: '#334155', + cursor: 'pointer', + display: 'inline-flex', + fontSize: 12, + fontWeight: 700, + minHeight: 30, + padding: '0 10px', +}; + +const endpointChoiceButtonActiveStyle: React.CSSProperties = { + ...endpointChoiceButtonStyle, + background: '#111827', + border: '1px solid #111827', + color: '#ffffff', }; const parameterGridStyle: React.CSSProperties = { @@ -726,24 +748,6 @@ const StudioMemberBindPanel: React.FC = ({ smokeInput, ]); - const serviceOptions = useMemo( - () => - services.map((service) => ({ - label: service.displayName || service.serviceId, - value: service.serviceId, - })), - [services], - ); - - const endpointOptions = useMemo( - () => - (selectedService?.endpoints ?? []).map((endpoint) => ({ - label: endpoint.displayName || endpoint.endpointId, - value: endpoint.endpointId, - })), - [selectedService?.endpoints], - ); - const snippetMap = useMemo(() => { if (!bindContract) { return { @@ -763,7 +767,6 @@ const StudioMemberBindPanel: React.FC = ({ const selectedSnippet = snippetMap[snippetTab]; const bindingCatalog: ScopeServiceBindingCatalogSnapshot | undefined = bindingsQuery.data; const bindingList = bindingCatalog?.bindings ?? []; - const hasMultiplePublishedServices = services.length > 1; const revisionList = revisionCatalogQuery.data?.revisions ?? []; const hasEndpointOptions = Boolean(selectedService?.endpoints.length); const endpointUnavailableMessage = @@ -949,12 +952,12 @@ const StudioMemberBindPanel: React.FC = ({ - {bindContract ? 'contract selected' : 'needs endpoint'} + {bindContract ? 'member contract selected' : 'needs endpoint'} {revisionList.length > 0 ? ( revisions ยท {revisionList.length} @@ -988,46 +991,53 @@ const StudioMemberBindPanel: React.FC = ({
- Published service - {hasMultiplePublishedServices ? ( - setSelectedEndpointId(String(value || ''))} - /> -
{endpointUnavailableMessage ? ( = ({ label: 'Contract details', children: bindContract ? (
+
+ + Published service + + + {bindContract.serviceId} + + + Platform diagnostic id for this member contract. + +
Workspace ID diff --git a/apps/aevatar-console-web/src/pages/studio/index.test.tsx b/apps/aevatar-console-web/src/pages/studio/index.test.tsx index 16b888129..c8a865943 100644 --- a/apps/aevatar-console-web/src/pages/studio/index.test.tsx +++ b/apps/aevatar-console-web/src/pages/studio/index.test.tsx @@ -4114,6 +4114,78 @@ describe("StudioPage", () => { expect(screen.queryByText("services:default,billing-api")).toBeNull(); }); + it("keeps Invoke on the selected member when a stale bind selection exists", async () => { + mockStudioMembers = [ + ...mockStudioMembers, + { + memberId: "support-member", + scopeId: "scope-1", + displayName: "support-member", + description: "Support member", + implementationKind: "workflow", + lifecycleStage: "bind_ready", + publishedServiceId: "support-service", + lastBoundRevisionId: "rev-support", + createdAt: "2026-04-27T08:00:00Z", + updatedAt: "2026-04-27T08:05:00Z", + }, + ]; + mockScopeRuntimeApi.listServices.mockResolvedValue([ + { + serviceId: "default", + displayName: "workspace-demo", + deploymentStatus: "Active", + primaryActorId: "actor-default", + endpoints: [ + { + endpointId: "chat", + displayName: "Chat", + kind: "chat", + description: "Chat with workspace-demo.", + requestTypeUrl: "", + responseTypeUrl: "", + }, + ], + }, + { + serviceId: "support-service", + displayName: "support-member", + deploymentStatus: "Active", + primaryActorId: "actor-support", + endpoints: [ + { + endpointId: "support-chat", + displayName: "Support chat", + kind: "chat", + description: "Chat with support.", + requestTypeUrl: "", + responseTypeUrl: "", + }, + ], + }, + ]); + + renderStudioPage( + "/studio?scopeId=scope-1&member=member%3Aworkspace-demo&step=bind" + ); + + expect(await screen.findByTestId("studio-bind-surface")).toBeTruthy(); + fireEvent.click(screen.getByRole("button", { name: "Select bind endpoint" })); + + await replaceStudioRoute( + "/studio?scopeId=scope-1&member=member%3Asupport-member&step=invoke" + ); + + expect(await screen.findByTestId("studio-invoke-surface")).toBeTruthy(); + await waitFor(() => { + expect(screen.getByText("service:support-service")).toBeTruthy(); + expect(screen.getByText("member:support-member")).toBeTruthy(); + expect(screen.getByText("services:support-service")).toBeTruthy(); + expect(screen.getByText("endpoint:support-chat")).toBeTruthy(); + }); + expect(screen.queryByText("service:default")).toBeNull(); + }); + it("shows an invoke empty state when a bound member has no endpoint data", async () => { mockScopeRuntimeApi.listServices.mockResolvedValueOnce([ { diff --git a/apps/aevatar-console-web/src/pages/studio/index.tsx b/apps/aevatar-console-web/src/pages/studio/index.tsx index 27a240867..e8d0c5dff 100644 --- a/apps/aevatar-console-web/src/pages/studio/index.tsx +++ b/apps/aevatar-console-web/src/pages/studio/index.tsx @@ -7226,10 +7226,11 @@ const StudioPage: React.FC = () => { const hasInvokeTargetMemberSelection = Boolean(workbenchStudioMemberId); const invokeTargetServiceId = - currentInvokeSelectionServiceId || - currentBindingSelectionServiceId || - currentSelectedMemberServiceId || - trimOptional(routeState.memberId); + hasInvokeTargetMemberSelection + ? currentSelectedMemberServiceId + : currentInvokeSelectionServiceId || + currentBindingSelectionServiceId || + trimOptional(routeState.memberId); const invokeTargetService = useMemo( () => { if (!invokeTargetServiceId) {