From a7d3c7f5204a1d62508d097eb2de729825417fd0 Mon Sep 17 00:00:00 2001 From: Day Dreamer <87640074+HugoXK@users.noreply.github.com> Date: Thu, 5 Nov 2026 01:31:44 +0800 Subject: [PATCH] workflow: a paused run is named Paused in the strip and in the Runs tab A Stop that could not release one of its Bots leaves the run live with `paused: true` and `releasePending`; docs/subsystems/workflow.md says it "shows as Paused". The stage chip and its meta line used the Pause action's name instead, and `runHistoryRows` dropped `paused`, so the Runs tab drew the frozen run as Running while it hands nothing on. Fixes #2423 Docs-not-needed: docs/subsystems/workflow.md already promises that a run left by a failed release "shows as Paused"; this makes both surfaces match it. --- .../workflow-paused-run-label.fixed.md | 1 + plugins/workflow/client.js | 11 +++--- plugins/workflow/test/client.test.js | 35 +++++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 changelog.d/workflow-paused-run-label.fixed.md diff --git a/changelog.d/workflow-paused-run-label.fixed.md b/changelog.d/workflow-paused-run-label.fixed.md new file mode 100644 index 000000000..ed9843f94 --- /dev/null +++ b/changelog.d/workflow-paused-run-label.fixed.md @@ -0,0 +1 @@ +A run frozen by a Stop that could not release its Bot is now named **Paused** wherever it is read: the stage strip says `Paused` instead of the `Pause` action's name, and the Runs tab keeps the run's `paused` flag instead of drawing the frozen run as `Running` while it hands nothing on. diff --git a/plugins/workflow/client.js b/plugins/workflow/client.js index 9b8a4f5f3..927a60f0c 100644 --- a/plugins/workflow/client.js +++ b/plugins/workflow/client.js @@ -150,6 +150,7 @@ window.__ModuleLoader__.load({ 'Show {count} more Bots': '展开另外 {count} 个 Bot', 'Show {count} more held tasks': '展开另外持有中的 {count} 个任务', 'Showing {shown} of {count} held tasks': '共有 {count} 个持有中的任务,当前显示 {shown} 个', + Paused: '已暂停', // Stage timeline (#1489). The key is the English line; service.js and // narrate.js keep the Chinese text only as the fallback for rows written // before the key existed. @@ -520,11 +521,11 @@ window.__ModuleLoader__.load({ function RunStrip({ workflow, run, focused, locked, onFocus, onPause, onResume, onStop, onTab, onOpenDrawer }) { const running = (run.running ?? []).map(id => nodeOf(workflow, id)).filter(Boolean) const chipClass = run.status === 'done' ? 'done' : (run.status === 'failed' ? 'failed' : (run.paused ? 'queued' : (running[0] ? 'running' : 'queued'))) - const chipLabel = run.status === 'done' ? tr('Done') : (run.status === 'failed' ? tr('Failed') : (run.paused ? tr('Pause') : (running[0] ? tr('Running') : tr('Queued')))) + const chipLabel = run.status === 'done' ? tr('Done') : (run.status === 'failed' ? tr('Failed') : (run.paused ? tr('Paused') : (running[0] ? tr('Running') : tr('Queued')))) const meta = run.status === 'failed' ? failureText(run) : run.paused - ? `${tr('Pause')} · ${running.map(node => node.name).join(' ‖ ') || '—'} · ${run.doneCount}/${run.nodeCount}` + ? `${tr('Paused')} · ${running.map(node => node.name).join(' ‖ ') || '—'} · ${run.doneCount}/${run.nodeCount}` : (running[0] ? `${tr('Running')} · ${running.map(node => node.name).join(' ‖ ')} · ${run.doneCount}/${run.nodeCount}` : (run.status === 'done' ? tr('Done') : tr('Waiting for a task'))) @@ -1065,6 +1066,7 @@ window.__ModuleLoader__.load({ live: true, task: run.task, status: run.status, + paused: run.paused === true, at: run.updatedAt, failure: run.failure, current: workflow.run?.id === run.id, @@ -1390,10 +1392,11 @@ window.__ModuleLoader__.load({ // #1618: keep HH:MM in its own non-shrinking column with a gap before the // status chip, and paint Failed once — skip wf-hist-reason when it only // repeats the chip label (generic Failed with no host detail). - const chipLabel = tr(STATUS_KEY[item.status] ?? 'Queued') + const chipLabel = item.paused === true ? tr('Paused') : tr(STATUS_KEY[item.status] ?? 'Queued') const reason = item.status === 'failed' ? failureText(item) : '' const showReason = Boolean(reason) && reason !== chipLabel - const chipClass = item.status === 'done' ? 'done' + const chipClass = item.paused === true ? 'queued' + : item.status === 'done' ? 'done' : item.status === 'running' ? 'running' : item.status === 'failed' ? 'failed' : 'queued' diff --git a/plugins/workflow/test/client.test.js b/plugins/workflow/test/client.test.js index 415ad6904..7615913d4 100644 --- a/plugins/workflow/test/client.test.js +++ b/plugins/workflow/test/client.test.js @@ -4684,3 +4684,38 @@ test('#2260: the held more-count promises only what the wider page reaches, and document.body = prevBody } }) + +// A run frozen by a Stop that could not release its Bot stays `status: 'running'` with +// `paused: true` (#1476), and nothing more is handed. The strip called that state by the +// action's name ("Pause"), and runHistoryRows dropped `paused` altogether, so the Runs tab +// drew the frozen run as "Running" — the one tab a reader checks to see what is going on. +test('a paused run is named Paused in the strip and in the Runs history', async () => { + const graph = delivery() + graph.activeCount = 1 + graph.runs = [{ ...graph.run, id: 'run-p', paused: true, status: 'running' }] + graph.run = graph.runs[0] + const call = async (name) => { + if (name === 'workflow/state') return { workflows: [graph], templates: [] } + return { ok: true, workflows: [graph] } + } + const view = mount(client.Section, { roster: { bots: [] }, call, actions: {} }) + try { + await settle() + findAll(view.tree, node => node.type === 'button' && textOf(node).includes('发布交付流水线'))[0].props.onClick() + await settle() + const strip = findAll(view.tree, node => node.props?.['data-run-id'] === 'run-p')[0] + assert.ok(strip, 'the paused run has a strip') + const stripChips = findAll(strip, node => String(node.props?.className ?? '').includes('wf-chip')).map(textOf) + assert.deepEqual(stripChips, ['Paused'], 'the strip names the frozen run Paused, not by the Pause action') + assert.equal(findAll(view.tree, node => node.type === 'button' && textOf(node) === 'Resume').length, 1, 'a paused run still offers Resume') + + button(view.tree, 'Progress').props.onClick() + await settle() + const rows = findAll(view.tree, node => String(node.props?.className ?? '').split(' ').includes('wf-hist')) + assert.equal(rows.length, 1, 'the Runs tab lists the paused run') + const chips = findAll(rows[0], node => String(node.props?.className ?? '').includes('wf-chip')).map(textOf) + assert.deepEqual(chips, ['Paused'], 'the Runs tab says Paused instead of Running') + } finally { + view.unmount() + } +})