From 7eb070dba1b576041cc63d2153c8a1ae8e9c8238 Mon Sep 17 00:00:00 2001 From: potter Date: Thu, 14 May 2026 18:31:09 +0800 Subject: [PATCH] Clarify invoke current run identity --- .../StudioMemberInvokePanel.test.tsx | 60 +++++++++++++++++++ .../components/StudioMemberInvokePanel.tsx | 21 ++++++- .../shared/navigation/runtimeRoutes.test.ts | 3 +- .../src/shared/navigation/runtimeRoutes.ts | 2 + .../src/shared/runs/scopeConsole.test.ts | 11 ++++ .../src/shared/runs/scopeConsole.ts | 9 ++- 6 files changed, 98 insertions(+), 8 deletions(-) diff --git a/apps/aevatar-console-web/src/pages/studio/components/StudioMemberInvokePanel.test.tsx b/apps/aevatar-console-web/src/pages/studio/components/StudioMemberInvokePanel.test.tsx index 84316be8c..fb79b451b 100644 --- a/apps/aevatar-console-web/src/pages/studio/components/StudioMemberInvokePanel.test.tsx +++ b/apps/aevatar-console-web/src/pages/studio/components/StudioMemberInvokePanel.test.tsx @@ -125,9 +125,13 @@ describe('StudioMemberInvokePanel', () => { expect(screen.getByText('Status')).toBeTruthy(); expect(screen.getByText('Run ID')).toBeTruthy(); expect(screen.getByText('Command ID')).toBeTruthy(); + expect(screen.getAllByText('Actor ID').length).toBeGreaterThanOrEqual(1); + expect(screen.getByText('Member ID')).toBeTruthy(); expect(screen.getByText('Elapsed')).toBeTruthy(); expect(screen.getByText('尚未开始')).toBeTruthy(); expect(screen.getByText('尚未发出')).toBeTruthy(); + expect(screen.getAllByText('actor-default').length).toBeGreaterThanOrEqual(1); + expect(screen.getByText('default')).toBeTruthy(); expect(screen.getByText('Member')).toBeTruthy(); expect(screen.getByText('Published Context')).toBeTruthy(); expect(screen.getByText('Revision')).toBeTruthy(); @@ -138,6 +142,7 @@ describe('StudioMemberInvokePanel', () => { expect(screen.getByText('Events')).toBeTruthy(); expect(screen.getByText('Advanced typed payload')).toBeTruthy(); expect(screen.getByTestId('studio-invoke-playground-actions')).toBeTruthy(); + expect(screen.getByRole('button', { name: '打开运行记录' })).toBeDisabled(); expect( screen.getByText('No conversation yet. Send a prompt to start the run.'), ).toBeTruthy(); @@ -364,6 +369,12 @@ describe('StudioMemberInvokePanel', () => { expect(inlineScope.getByText('Actor ID')).toBeTruthy(); expect(inlineScope.getByText('actor-1')).toBeTruthy(); expect(inlineScope.getByText('Duration')).toBeTruthy(); + expect(screen.getByRole('button', { name: '打开运行记录' })).toBeEnabled(); + + fireEvent.click(screen.getByRole('button', { name: '打开运行记录' })); + + expect(window.location.pathname).toBe('/runtime/runs'); + expect(new URLSearchParams(window.location.search).get('runId')).toBe('run-1'); fireEvent.change(screen.getByLabelText('调用请求输入'), { target: { @@ -374,6 +385,55 @@ describe('StudioMemberInvokePanel', () => { expect(screen.getByLabelText('调用请求输入')).toHaveValue('Overwrite prompt'); }); + it('does not enable Runs navigation when invoke only returns a command id', async () => { + (runtimeRunsApi.invokeEndpoint as jest.Mock).mockResolvedValueOnce({ + accepted: true, + commandId: 'cmd-only', + targetActorId: 'actor-1', + }); + + render( + React.createElement(StudioMemberInvokePanel, { + memberId: 'default', + scopeId: 'scope-1', + services: [ + { + deploymentStatus: 'Active', + displayName: 'workspace-demo', + endpoints: [ + { + description: 'Send a structured request into the member.', + displayName: 'Submit', + endpointId: 'submit', + kind: 'invoke', + requestTypeUrl: 'type.googleapis.com/example.Submit', + responseTypeUrl: 'type.googleapis.com/example.SubmitResult', + }, + ], + kind: 'service', + namespace: 'default', + primaryActorId: 'actor-default', + serviceId: 'default', + }, + ], + }), + ); + + fireEvent.change(await screen.findByLabelText('调用请求输入'), { + target: { + value: 'Dispatch this typed command.', + }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Invoke' })); + + await waitFor(() => { + expect(runtimeRunsApi.invokeEndpoint).toHaveBeenCalled(); + expect(screen.getAllByText('cmd-only').length).toBeGreaterThanOrEqual(1); + }); + + expect(screen.getByRole('button', { name: '打开运行记录' })).toBeDisabled(); + }); + it('renders a clear empty state when no selected member is available for invoke', async () => { render( React.createElement(StudioMemberInvokePanel, { diff --git a/apps/aevatar-console-web/src/pages/studio/components/StudioMemberInvokePanel.tsx b/apps/aevatar-console-web/src/pages/studio/components/StudioMemberInvokePanel.tsx index d968e6719..72a6b253d 100644 --- a/apps/aevatar-console-web/src/pages/studio/components/StudioMemberInvokePanel.tsx +++ b/apps/aevatar-console-web/src/pages/studio/components/StudioMemberInvokePanel.tsx @@ -378,6 +378,9 @@ const StudioMemberInvokePanel: React.FC = ({ ); const runIdLabel = trimOptional(invokeResult.runId) || '尚未开始'; const commandIdLabel = trimOptional(invokeResult.commandId) || '尚未发出'; + const actorIdLabel = + trimOptional(invokeResult.actorId) || currentMemberActorId || '尚未分配'; + const memberIdLabel = normalizedMemberId || '未选中成员'; const endpointLabel = selectedEndpoint?.displayName || selectedEndpointId || '—'; useEffect(() => { @@ -1028,7 +1031,8 @@ const StudioMemberInvokePanel: React.FC = ({ ]); const handleOpenRuns = useCallback(() => { - if (!scopeId || !normalizedMemberId || !selectedEndpoint) { + const currentRunId = trimOptional(invokeResult.runId); + if (!scopeId || !normalizedMemberId || !selectedEndpoint || !currentRunId) { return; } @@ -1066,6 +1070,7 @@ const StudioMemberInvokePanel: React.FC = ({ payloadTypeUrl: currentPayloadTypeUrl || undefined, prompt: currentPrompt || undefined, returnTo: returnTo || undefined, + runId: currentRunId, scopeId, serviceId: selectedService?.serviceId, }), @@ -1155,6 +1160,18 @@ const StudioMemberInvokePanel: React.FC = ({ {commandIdLabel} +
+
Actor ID
+
+ {actorIdLabel} +
+
+
+
Member ID
+
+ {memberIdLabel} +
+
Elapsed
{runElapsedLabel}
@@ -1185,7 +1202,7 @@ const StudioMemberInvokePanel: React.FC = ({ effectiveResponseTypeUrl={effectiveResponseTypeUrl} endpointKind={selectedEndpoint?.kind || 'command'} formError={formError} - hasOpenRunsTarget={Boolean(scopeId && selectedEndpoint)} + hasOpenRunsTarget={Boolean(trimOptional(invokeResult.runId))} invokeStatus={invokeResult.status} isChatEndpoint={isChatEndpoint} layout="dock" diff --git a/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.test.ts b/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.test.ts index c97a2da9c..7320e6645 100644 --- a/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.test.ts +++ b/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.test.ts @@ -26,13 +26,14 @@ describe('runtimeRoutes', () => { expect( buildRuntimeRunsHref({ actorId: 'actor://selected', + runId: 'run-1', returnTo: buildRuntimeExplorerHref({ actorId: 'actor://selected', runId: 'run-1', }), }), ).toContain( - 'returnTo=%2Fruntime%2Fexplorer%2Fdetail%3FactorId%3Dactor%253A%252F%252Fselected%26runId%3Drun-1', + 'actorId=actor%3A%2F%2Fselected&runId=run-1&returnTo=%2Fruntime%2Fexplorer%2Fdetail%3FactorId%3Dactor%253A%252F%252Fselected%26runId%3Drun-1', ); }); diff --git a/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.ts b/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.ts index a4ae11e67..d9853dac5 100644 --- a/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.ts +++ b/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.ts @@ -58,6 +58,7 @@ export function buildRuntimeRunsHref(options?: { payloadTypeUrl?: string; payloadBase64?: string; actorId?: string; + runId?: string; draftKey?: string; returnTo?: string; }): string { @@ -71,6 +72,7 @@ export function buildRuntimeRunsHref(options?: { payloadTypeUrl: options?.payloadTypeUrl, payloadBase64: options?.payloadBase64, actorId: options?.actorId, + runId: options?.runId, draftKey: options?.draftKey, returnTo: options?.returnTo, }); diff --git a/apps/aevatar-console-web/src/shared/runs/scopeConsole.test.ts b/apps/aevatar-console-web/src/shared/runs/scopeConsole.test.ts index 9c16f82fa..2958fdc33 100644 --- a/apps/aevatar-console-web/src/shared/runs/scopeConsole.test.ts +++ b/apps/aevatar-console-web/src/shared/runs/scopeConsole.test.ts @@ -125,6 +125,17 @@ describe("scopeConsole", () => { correlationId: "corr-1", runId: "run-1", }); + expect( + extractRuntimeInvokeReceipt({ + commandId: "cmd-only", + targetActorId: "actor://svc", + }) + ).toEqual({ + actorId: "actor://svc", + commandId: "cmd-only", + correlationId: "", + runId: "", + }); expect( getPreferredScopeConsoleServiceId( diff --git a/apps/aevatar-console-web/src/shared/runs/scopeConsole.ts b/apps/aevatar-console-web/src/shared/runs/scopeConsole.ts index 23bfd6b6f..c0afa7629 100644 --- a/apps/aevatar-console-web/src/shared/runs/scopeConsole.ts +++ b/apps/aevatar-console-web/src/shared/runs/scopeConsole.ts @@ -169,8 +169,8 @@ export function extractRuntimeInvokeReceipt( response, "request_id", "requestId", - "command_id", - "commandId" + "run_id", + "runId" ); return { @@ -180,9 +180,8 @@ export function extractRuntimeInvokeReceipt( "targetActorId", "actorId" ), - commandId: readResponseField(response, "command_id", "commandId") || runId, - correlationId: - readResponseField(response, "correlation_id", "correlationId") || runId, + commandId: readResponseField(response, "command_id", "commandId"), + correlationId: readResponseField(response, "correlation_id", "correlationId"), runId, }; }