mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
371f06e5ac
* feat(cli): bulk-cancel runs in a single operation Replace the per-run cancel loop in `workflow cancel` with one `cancelRuns` call, validate `--limit` (1-500), print a compact outcome summary with per-run lines for surfaced failures, and exit nonzero only when a run fails. The bulk logic lives in a dependency-injected `performBulkCancel` helper so it is unit-testable without an oclif harness. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cli): address bulk cancel review feedback * feat(web): bulk-cancel selected runs in a single request Thread a bulkCancelRuns action through the server action, RPC route, rpc-client, and client wrappers, backed by core's cancelRuns. The runs table now cancels the selected pending/running runs in one call, caps a batch at BULK_CANCEL_MAX_RUN_IDS (disabling the button with guidance above the cap), and reports a single outcome-summary toast covering only the categories that occurred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
2.7 KiB
TypeScript
102 lines
2.7 KiB
TypeScript
import type { BulkCancelWorkflowRunsResult } from '@workflow/world';
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
bulkCancelToastSeverity,
|
|
shouldRetainSelectionAfterBulkCancel,
|
|
} from './bulk-cancel-ui';
|
|
|
|
type Summary = BulkCancelWorkflowRunsResult['summary'];
|
|
|
|
function summary(overrides: Partial<Summary>): Summary {
|
|
return {
|
|
requested: 0,
|
|
cancelled: 0,
|
|
alreadyCancelled: 0,
|
|
notCancellable: 0,
|
|
notFound: 0,
|
|
failed: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('bulkCancelToastSeverity', () => {
|
|
it('is success when every run cancelled', () => {
|
|
expect(
|
|
bulkCancelToastSeverity(summary({ requested: 2, cancelled: 2 }))
|
|
).toBe('success');
|
|
});
|
|
|
|
it('treats already-cancelled as an idempotent success, not an error', () => {
|
|
// A concurrent caller cancelled everything first: only alreadyCancelled,
|
|
// zero cancelled. This must not be an error.
|
|
expect(
|
|
bulkCancelToastSeverity(summary({ requested: 2, alreadyCancelled: 2 }))
|
|
).toBe('success');
|
|
});
|
|
|
|
it('warns when a genuine problem accompanies some success', () => {
|
|
expect(
|
|
bulkCancelToastSeverity(
|
|
summary({ requested: 2, cancelled: 1, failed: 1 })
|
|
)
|
|
).toBe('warning');
|
|
expect(
|
|
bulkCancelToastSeverity(
|
|
summary({ requested: 2, alreadyCancelled: 1, notFound: 1 })
|
|
)
|
|
).toBe('warning');
|
|
});
|
|
|
|
it('is an error only when nothing succeeded', () => {
|
|
expect(bulkCancelToastSeverity(summary({ requested: 1, failed: 1 }))).toBe(
|
|
'error'
|
|
);
|
|
expect(
|
|
bulkCancelToastSeverity(summary({ requested: 1, notCancellable: 1 }))
|
|
).toBe('error');
|
|
});
|
|
});
|
|
|
|
describe('shouldRetainSelectionAfterBulkCancel', () => {
|
|
it('retains retryable failures', () => {
|
|
expect(
|
|
shouldRetainSelectionAfterBulkCancel({
|
|
runId: 'r',
|
|
outcome: 'failed',
|
|
code: 'internal_error',
|
|
retryable: true,
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it('clears non-retryable failures and every terminal outcome', () => {
|
|
expect(
|
|
shouldRetainSelectionAfterBulkCancel({
|
|
runId: 'r',
|
|
outcome: 'failed',
|
|
code: 'internal_error',
|
|
retryable: false,
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
shouldRetainSelectionAfterBulkCancel({ runId: 'r', outcome: 'cancelled' })
|
|
).toBe(false);
|
|
expect(
|
|
shouldRetainSelectionAfterBulkCancel({
|
|
runId: 'r',
|
|
outcome: 'already_cancelled',
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
shouldRetainSelectionAfterBulkCancel({ runId: 'r', outcome: 'not_found' })
|
|
).toBe(false);
|
|
expect(
|
|
shouldRetainSelectionAfterBulkCancel({
|
|
runId: 'r',
|
|
outcome: 'not_cancellable',
|
|
status: 'completed',
|
|
})
|
|
).toBe(false);
|
|
});
|
|
});
|