diff --git a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts index 11d221e0cf..c5eccdc5c4 100644 --- a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts +++ b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts @@ -181,7 +181,7 @@ describe('executeBulkOperation', () => { }) }) - test('renders success message when bulk operation returns without user errors', async () => { + test('renders running message when bulk operation returns without user errors', async () => { const query = '{ products { edges { node { id } } } }' const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = { bulkOperation: createdBulkOperation, @@ -197,7 +197,8 @@ describe('executeBulkOperation', () => { expect(renderSuccess).toHaveBeenCalledWith( expect.objectContaining({ - headline: 'Bulk operation started.', + headline: 'Bulk operation is running.', + body: ['Monitor its progress with:\n', {command: expect.stringContaining('shopify app bulk status')}], }), ) }) @@ -453,6 +454,73 @@ describe('executeBulkOperation', () => { ) }) + test('renders running message when quickWatchBulkOperation returns COMPLETED status (does not download results)', async () => { + const query = '{ products { edges { node { id } } } }' + const completedOperation = { + ...createdBulkOperation, + status: 'COMPLETED' as const, + url: 'https://example.com/download', + objectCount: '100', + } + const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = { + bulkOperation: createdBulkOperation, + userErrors: [], + } + + vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse) + vi.mocked(shortBulkOperationPoll).mockResolvedValue(completedOperation) + + await executeBulkOperation({ + organization: mockOrganization, + remoteApp: mockRemoteApp, + store: mockStore, + query, + watch: false, + }) + + expect(renderSuccess).toHaveBeenCalledWith( + expect.objectContaining({ + headline: 'Bulk operation is running.', + body: ['Monitor its progress with:\n', {command: expect.stringContaining('shopify app bulk status')}], + }), + ) + expect(downloadBulkOperationResults).not.toHaveBeenCalled() + }) + + test.each(['FAILED', 'CANCELED', 'EXPIRED'] as const)( + 'renders error when quickWatchBulkOperation returns %s status', + async (status) => { + const query = '{ products { edges { node { id } } } }' + const errorOperation = { + ...createdBulkOperation, + status, + objectCount: '0', + } + const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = { + bulkOperation: createdBulkOperation, + userErrors: [], + } + + vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse) + vi.mocked(shortBulkOperationPoll).mockResolvedValue(errorOperation) + + await executeBulkOperation({ + organization: mockOrganization, + remoteApp: mockRemoteApp, + store: mockStore, + query, + watch: false, + }) + + expect(renderError).toHaveBeenCalledWith( + expect.objectContaining({ + headline: expect.any(String), + customSections: expect.any(Array), + }), + ) + }, + ) + test('writes results to file when --output-file flag is provided', async () => { const query = '{ products { edges { node { id } } } }' const outputFile = '/tmp/results.jsonl' diff --git a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts index eecac70803..c29ba58edd 100644 --- a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts +++ b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts @@ -121,7 +121,16 @@ export async function executeBulkOperation(input: ExecuteBulkOperationInput): Pr } } else { const operation = await shortBulkOperationPoll(adminSession, createdOperation.id) - await renderBulkOperationResult(operation, outputFile) + const errorStatuses = ['FAILED', 'CANCELED', 'EXPIRED'] + if (errorStatuses.includes(operation.status)) { + await renderBulkOperationResult(operation, outputFile) + } else { + renderSuccess({ + headline: 'Bulk operation is running.', + body: statusCommandHelpMessage(operation.id), + customSections: [{body: [{list: {items: [outputContent`ID: ${outputToken.cyan(operation.id)}`.value]}}]}], + }) + } } } else { renderWarning({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b183faf400..9fc1b20697 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14384,15 +14384,6 @@ snapshots: msw: 2.8.7(@types/node@24.7.0)(typescript@5.8.3) vite: 6.4.1(@types/node@18.19.70)(jiti@2.4.2)(sass@1.89.1)(yaml@2.8.1) - '@vitest/mocker@3.2.1(msw@2.8.7(@types/node@24.7.0)(typescript@5.8.3))(vite@6.4.1(@types/node@24.7.0)(jiti@2.4.2)(sass@1.89.1)(yaml@2.8.1))': - dependencies: - '@vitest/spy': 3.2.1 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - msw: 2.8.7(@types/node@24.7.0)(typescript@5.8.3) - vite: 6.4.1(@types/node@24.7.0)(jiti@2.4.2)(sass@1.89.1)(yaml@2.8.1) - '@vitest/pretty-format@3.2.1': dependencies: tinyrainbow: 2.0.0 @@ -19992,7 +19983,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.1 - '@vitest/mocker': 3.2.1(msw@2.8.7(@types/node@24.7.0)(typescript@5.8.3))(vite@6.4.1(@types/node@24.7.0)(jiti@2.4.2)(sass@1.89.1)(yaml@2.8.1)) + '@vitest/mocker': 3.2.1(msw@2.8.7(@types/node@24.7.0)(typescript@5.8.3))(vite@6.4.1(@types/node@18.19.70)(jiti@2.4.2)(sass@1.89.1)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.1 '@vitest/runner': 3.2.1 '@vitest/snapshot': 3.2.1