Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ const workflowActivityVNextMessages = {
'Choose a creation method now. Your input stays on this page while you restore access.',
'workflowActivityVNext.new.yaml': 'Workflow YAML',
'workflowActivityVNext.run.backAria': 'Back to Activity',
'workflowActivityVNext.run.changeConfiguration': 'Change configuration',
'workflowActivityVNext.run.commandId': 'Request ID',
'workflowActivityVNext.run.code': 'Code',
'workflowActivityVNext.run.completedSteps': 'Completed steps',
Expand All @@ -395,13 +396,17 @@ const workflowActivityVNextMessages = {
'workflowActivityVNext.run.confirmTitle': 'Confirm new run',
'workflowActivityVNext.run.correlationId': 'Tracking ID',
'workflowActivityVNext.run.cost': 'Cost',
'workflowActivityVNext.run.costWarning':
'This action may incur new model or tool cost.',
'workflowActivityVNext.run.definitionRevision': 'Definition revision',
'workflowActivityVNext.run.diagnostics': 'Diagnostics',
'workflowActivityVNext.run.error': 'Final error',
'workflowActivityVNext.run.failedSummary': 'The run did not complete.',
'workflowActivityVNext.run.fixAccess': 'Fix access',
'workflowActivityVNext.run.description':
'Review the result, steps, and history for this run.',
'workflowActivityVNext.run.title': 'Run details',
'workflowActivityVNext.run.forkAccepted': 'New run started',
'workflowActivityVNext.run.forkAccepted': 'New run accepted',
'workflowActivityVNext.run.forkAcceptedDescription':
'Open Activity to follow its progress.',
'workflowActivityVNext.run.graph': 'Graph',
Expand All @@ -420,10 +425,20 @@ const workflowActivityVNextMessages = {
'workflowActivityVNext.run.promptTokens': 'Prompt tokens',
'workflowActivityVNext.run.requestedSteps': 'Requested steps',
'workflowActivityVNext.run.requestParameters': 'Request parameters',
'workflowActivityVNext.run.priorOutputs': 'Prior outputs',
'workflowActivityVNext.run.priorOutputsNotReused':
'Prior step outputs will not be reused.',
'workflowActivityVNext.run.priorOutputsReused':
'Prior step outputs will be reused.',
'workflowActivityVNext.run.relatedRuns': 'Related runs',
'workflowActivityVNext.run.retry': 'Retry failed step',
'workflowActivityVNext.run.retryAuthorityMissing':
'Retry eligibility was not provided by the run service.',
'workflowActivityVNext.run.retryUnavailable':
'Retry is available when one step has failed.',
'workflowActivityVNext.run.runAgain': 'Run again',
'workflowActivityVNext.run.runAgainAuthorityMissing':
'Run-again eligibility was not provided by the run service.',
'workflowActivityVNext.run.runAgainUnavailable':
"Run again isn't available for this run.",
'workflowActivityVNext.run.roleReplies': 'Role replies',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ const workflowActivityVNextMessages: Record<keyof typeof enUSMessages, string> =
'现在可以先选择创建方式;恢复访问权限期间,你的输入会保留在此页面。',
'workflowActivityVNext.new.yaml': '工作流 YAML',
'workflowActivityVNext.run.backAria': '返回活动列表',
'workflowActivityVNext.run.changeConfiguration': '更改配置',
'workflowActivityVNext.run.commandId': '请求 ID',
'workflowActivityVNext.run.code': '代码',
'workflowActivityVNext.run.completedSteps': '已完成步骤',
Expand All @@ -370,12 +371,16 @@ const workflowActivityVNextMessages: Record<keyof typeof enUSMessages, string> =
'workflowActivityVNext.run.confirmTitle': '确认新运行',
'workflowActivityVNext.run.correlationId': '跟踪 ID',
'workflowActivityVNext.run.cost': '费用',
'workflowActivityVNext.run.costWarning':
'此操作可能产生新的模型或工具费用。',
'workflowActivityVNext.run.definitionRevision': '定义版本',
'workflowActivityVNext.run.diagnostics': '诊断',
'workflowActivityVNext.run.error': '最终错误',
'workflowActivityVNext.run.failedSummary': '此次运行未完成。',
'workflowActivityVNext.run.fixAccess': '修复访问权限',
'workflowActivityVNext.run.description': '查看此次运行的结果、步骤和历史。',
'workflowActivityVNext.run.title': '运行详情',
'workflowActivityVNext.run.forkAccepted': '新运行已开始',
'workflowActivityVNext.run.forkAccepted': '新运行已接受',
'workflowActivityVNext.run.forkAcceptedDescription':
'打开活动记录查看进度。',
'workflowActivityVNext.run.graph': '图',
Expand All @@ -394,10 +399,18 @@ const workflowActivityVNextMessages: Record<keyof typeof enUSMessages, string> =
'workflowActivityVNext.run.promptTokens': '输入 Token',
'workflowActivityVNext.run.requestedSteps': '已请求步骤',
'workflowActivityVNext.run.requestParameters': '请求参数',
'workflowActivityVNext.run.priorOutputs': '先前输出',
'workflowActivityVNext.run.priorOutputsNotReused': '不会复用先前步骤输出。',
'workflowActivityVNext.run.priorOutputsReused': '将复用先前步骤输出。',
'workflowActivityVNext.run.relatedRuns': '相关运行',
'workflowActivityVNext.run.retry': '重试失败步骤',
'workflowActivityVNext.run.retryAuthorityMissing':
'运行服务未提供重试资格。',
'workflowActivityVNext.run.retryUnavailable':
'存在一个失败步骤时才能重试。',
'workflowActivityVNext.run.runAgain': '再次运行',
'workflowActivityVNext.run.runAgainAuthorityMissing':
'运行服务未提供再次运行资格。',
'workflowActivityVNext.run.runAgainUnavailable':
'此次运行暂时无法重新开始。',
'workflowActivityVNext.run.roleReplies': '角色回复数',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,28 @@ function buildRunDetail() {
input: 'Investigate checkout latency',
finalOutput: '',
finalError: 'Approval timed out',
recovery: {
recommendedAction: 'retry_failed_step',
definitionRevision: 'revision-42',
retry: {
eligible: true,
unavailableReason: '',
startAtStepId: 'step-failed',
reusesPriorStepOutputs: true,
mayIncurCost: true,
},
runAgain: {
eligible: true,
unavailableReason: '',
startAtStepId: 'step-root',
reusesPriorStepOutputs: false,
mayIncurCost: true,
},
lineage: {
parentRunId: 'run-parent-beta',
childRunIds: ['run-child-gamma'],
},
},
diagnostics: [
{
timestampUtc: '2026-08-04T10:01:00Z',
Expand Down Expand Up @@ -229,6 +251,15 @@ describe('Workflow Activity vNext run detail recovery', () => {
expect(
within(confirmation).getByText('Investigate checkout latency'),
).toBeInTheDocument();
expect(within(confirmation).getByText('revision-42')).toBeInTheDocument();
expect(
within(confirmation).getByText('Prior step outputs will be reused.'),
).toBeInTheDocument();
expect(
within(confirmation).getByText(
'This action may incur new model or tool cost.',
),
).toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: 'Confirm retry' }));

Expand All @@ -239,7 +270,7 @@ describe('Workflow Activity vNext run detail recovery', () => {
input: 'Investigate checkout latency',
}),
);
expect(await screen.findByText('New run started')).toBeInTheDocument();
expect(await screen.findByText('New run accepted')).toBeInTheDocument();
expect(screen.queryByText('actor-new-alpha')).not.toBeVisible();
expect(screen.queryByText('command-alpha')).not.toBeVisible();
expect(screen.queryByText('correlation-alpha')).not.toBeVisible();
Expand All @@ -249,6 +280,143 @@ describe('Workflow Activity vNext run detail recovery', () => {
expect(screen.queryByText(/state version/i)).not.toBeInTheDocument();
});

it('does not infer retry eligibility from failed steps or diagnostic strings', async () => {
const run = buildRunDetail();
mockWorkflowActivityApi.getRun.mockResolvedValue({
...run,
recovery: null,
diagnostics: [
{
...run.diagnostics[0],
code: 'AUTHORIZATION_FAILED',
message: 'Access must be restored before another attempt.',
},
],
});

renderWithQueryClient(
<RunDetailPage runId="run-source-alpha" scopeId="scope-alpha" />,
);

const retry = await screen.findByRole('button', {
name: 'Retry failed step',
});
expect(retry).toHaveAttribute('aria-disabled', 'true');
expect(retry).not.toBeDisabled();
retry.focus();
expect(retry).toHaveFocus();
const reason = screen.getByText(
'Retry eligibility was not provided by the run service.',
);
expect(reason).toBeVisible();
expect(retry).toHaveAttribute('aria-describedby', reason.id);

fireEvent.click(retry);
expect(
screen.queryByRole('dialog', { name: 'Confirm new run' }),
).not.toBeInTheDocument();
expect(mockWorkflowActivityApi.forkRun).not.toHaveBeenCalled();
});

it('uses the typed access recommendation instead of diagnostic text', async () => {
const run = buildRunDetail();
mockWorkflowActivityApi.getRun.mockResolvedValue({
...run,
recovery: {
...run.recovery,
recommendedAction: 'fix_access',
retry: {
eligible: false,
unavailableReason: 'Restore service access before retrying.',
startAtStepId: null,
reusesPriorStepOutputs: false,
mayIncurCost: false,
},
runAgain: {
eligible: false,
unavailableReason: 'Restore service access before running again.',
startAtStepId: null,
reusesPriorStepOutputs: false,
mayIncurCost: false,
},
},
diagnostics: [
{
...run.diagnostics[0],
code: 'RATE_LIMIT_TEXT_ONLY',
message: 'Retry this request later.',
},
],
});

renderWithQueryClient(
<RunDetailPage runId="run-source-alpha" scopeId="scope-alpha" />,
);

expect(
await screen.findByRole('link', { name: 'Fix access' }),
).toHaveAttribute(
'href',
'/scopes/scope-alpha/workflow-activity-vnext/settings?section=account',
);
expect(
screen.getByRole('button', { name: 'Retry failed step' }),
).toHaveAttribute('aria-disabled', 'true');
expect(
screen.getByText('Restore service access before retrying.'),
).toBeVisible();
});

it('does not present an unavailable recommended retry as primary', async () => {
const run = buildRunDetail();
mockWorkflowActivityApi.getRun.mockResolvedValue({
...run,
recovery: {
...run.recovery,
recommendedAction: 'retry_failed_step',
retry: {
eligible: false,
unavailableReason: 'This failure cannot be retried safely.',
startAtStepId: null,
reusesPriorStepOutputs: false,
mayIncurCost: false,
},
},
});

renderWithQueryClient(
<RunDetailPage runId="run-source-alpha" scopeId="scope-alpha" />,
);

const retry = await screen.findByRole('button', {
name: 'Retry failed step',
});
expect(retry).toHaveAttribute('aria-disabled', 'true');
expect(retry).not.toHaveClass('ant-btn-primary');
});

it('renders related runs only from typed lineage run identities', async () => {
renderWithQueryClient(
<RunDetailPage runId="run-source-alpha" scopeId="scope-alpha" />,
);

expect(
await screen.findByRole('link', { name: 'run-parent-beta' }),
).toHaveAttribute(
'href',
'/scopes/scope-alpha/workflow-activity-vnext/activity/run-parent-beta',
);
expect(
screen.getByRole('link', { name: 'run-child-gamma' }),
).toHaveAttribute(
'href',
'/scopes/scope-alpha/workflow-activity-vnext/activity/run-child-gamma',
);
expect(
screen.queryByRole('link', { name: 'actor-new-alpha' }),
).not.toBeInTheDocument();
});

it('reports a retry failure with a toast and keeps server detail out of the page', async () => {
mockWorkflowActivityApi.forkRun.mockRejectedValue(
new Error('POST /api/workflow/runs/fork returned 503'),
Expand Down Expand Up @@ -276,7 +444,7 @@ describe('Workflow Activity vNext run detail recovery', () => {
).not.toBeInTheDocument();
});

it('keeps committed detail visible and disables run again when graph evidence fails', async () => {
it('keeps typed run-again eligibility when graph presentation fails', async () => {
mockWorkflowActivityApi.getRunGraph.mockRejectedValue(
new Error('graph offline'),
);
Expand All @@ -290,7 +458,10 @@ describe('Workflow Activity vNext run detail recovery', () => {
expect(
await screen.findByText('Run graph unavailable'),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Run again' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Run again' })).toHaveAttribute(
'aria-disabled',
'false',
);
});

it('keeps raw run and step errors behind technical details', async () => {
Expand Down
Loading
Loading