/**
 * Tests for runAllTasks concurrency support (worker pool)
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { TaskInfo } from '../infra/task/index.js';

// Mock dependencies before importing the module under test
vi.mock('../infra/config/index.js', () => ({
  loadPieceByIdentifier: vi.fn(),
  isPiecePath: vi.fn(() => false),
  loadGlobalConfig: vi.fn(() => ({
    language: 'en',
    defaultPiece: 'default',
    logLevel: 'info',
    concurrency: 1,
    taskPollIntervalMs: 500,
  })),
}));

import { loadGlobalConfig } from '../infra/config/index.js';
const mockLoadGlobalConfig = vi.mocked(loadGlobalConfig);

const {
  mockClaimNextTasks,
  mockCompleteTask,
  mockFailTask,
  mockRecoverInterruptedRunningTasks,
  mockNotifySuccess,
  mockNotifyError,
  mockSendSlackNotification,
  mockGetSlackWebhookUrl,
} = vi.hoisted(() => ({
  mockClaimNextTasks: vi.fn(),
  mockCompleteTask: vi.fn(),
  mockFailTask: vi.fn(),
  mockRecoverInterruptedRunningTasks: vi.fn(),
  mockNotifySuccess: vi.fn(),
  mockNotifyError: vi.fn(),
  mockSendSlackNotification: vi.fn(),
  mockGetSlackWebhookUrl: vi.fn(),
}));

vi.mock('../infra/task/index.js', async (importOriginal) => ({
  ...(await importOriginal<Record<string, unknown>>()),
  TaskRunner: vi.fn().mockImplementation(() => ({
    claimNextTasks: mockClaimNextTasks,
    completeTask: mockCompleteTask,
    failTask: mockFailTask,
    recoverInterruptedRunningTasks: mockRecoverInterruptedRunningTasks,
  })),
}));

vi.mock('../infra/task/clone.js', async (importOriginal) => ({
  ...(await importOriginal<Record<string, unknown>>()),
  createSharedClone: vi.fn(),
  removeClone: vi.fn(),
}));

vi.mock('../infra/task/git.js', async (importOriginal) => ({
  ...(await importOriginal<Record<string, unknown>>()),
  getCurrentBranch: vi.fn(() => 'main'),
}));

vi.mock('../infra/task/autoCommit.js', async (importOriginal) => ({
  ...(await importOriginal<Record<string, unknown>>()),
  autoCommitAndPush: vi.fn(),
}));

vi.mock('../infra/task/summarize.js', async (importOriginal) => ({
  ...(await importOriginal<Record<string, unknown>>()),
  summarizeTaskName: vi.fn(),
}));

vi.mock('../shared/ui/index.js', () => ({
  header: vi.fn(),
  info: vi.fn(),
  warn: vi.fn(),
  error: vi.fn(),
  success: vi.fn(),
  status: vi.fn(),
  blankLine: vi.fn(),
}));

vi.mock('../shared/utils/index.js', async (importOriginal) => ({
  ...(await importOriginal<Record<string, unknown>>()),
  createLogger: () => ({
    info: vi.fn(),
    debug: vi.fn(),
    error: vi.fn(),
  }),
  getErrorMessage: vi.fn((e) => e.message),
  notifySuccess: mockNotifySuccess,
  notifyError: mockNotifyError,
  sendSlackNotification: mockSendSlackNotification,
  getSlackWebhookUrl: mockGetSlackWebhookUrl,
}));

vi.mock('../features/tasks/execute/pieceExecution.js', () => ({
  executePiece: vi.fn(() => Promise.resolve({ success: true })),
}));

vi.mock('../shared/context.js', () => ({
  isQuietMode: vi.fn(() => false),
}));

vi.mock('../shared/constants.js', () => ({
  DEFAULT_PIECE_NAME: 'default',
  DEFAULT_LANGUAGE: 'en',
}));

vi.mock('../infra/github/index.js', () => ({
  createPullRequest: vi.fn(),
  buildPrBody: vi.fn(),
  pushBranch: vi.fn(),
}));

vi.mock('../infra/claude/index.js', () => ({
  interruptAllQueries: vi.fn(),
  detectRuleIndex: vi.fn(),
}));

vi.mock('../agents/ai-judge.js', () => ({
  callAiJudge: vi.fn(),
}));

vi.mock('../shared/exitCodes.js', () => ({
  EXIT_SIGINT: 130,
}));

vi.mock('../shared/i18n/index.js', () => ({
  getLabel: vi.fn((key: string) => key),
}));

import { info, header, status, success, error as errorFn } from '../shared/ui/index.js';
import { runAllTasks } from '../features/tasks/index.js';
import { executePiece } from '../features/tasks/execute/pieceExecution.js';
import { loadPieceByIdentifier } from '../infra/config/index.js';

const mockInfo = vi.mocked(info);
const mockHeader = vi.mocked(header);
const mockStatus = vi.mocked(status);
const mockSuccess = vi.mocked(success);
const mockError = vi.mocked(errorFn);
const mockExecutePiece = vi.mocked(executePiece);
const mockLoadPieceByIdentifier = vi.mocked(loadPieceByIdentifier);

function createTask(name: string): TaskInfo {
  return {
    name,
    content: `Task: ${name}`,
    filePath: `/tasks/${name}.yaml`,
    createdAt: '2026-02-09T00:00:00.000Z',
    status: 'pending',
    data: null,
  };
}

beforeEach(() => {
  vi.clearAllMocks();
  mockRecoverInterruptedRunningTasks.mockReturnValue(0);
});

describe('runAllTasks concurrency', () => {
  describe('sequential execution (concurrency=1)', () => {
    beforeEach(() => {
      mockLoadGlobalConfig.mockReturnValue({
        language: 'en',
        defaultPiece: 'default',
        logLevel: 'info',
        notificationSound: true,
        notificationSoundEvents: { runComplete: true, runAbort: true },
        concurrency: 1,
        taskPollIntervalMs: 500,
      });
    });

    it('should show no-tasks message when no tasks exist', async () => {
      // Given: No pending tasks
      mockClaimNextTasks.mockReturnValue([]);

      // When
      await runAllTasks('/project');

      // Then
      expect(mockInfo).toHaveBeenCalledWith('No pending tasks in .takt/tasks.yaml');
    });

    it('should execute tasks sequentially via worker pool when concurrency is 1', async () => {
      // Given: Two tasks available sequentially
      const task1 = createTask('task-1');
      const task2 = createTask('task-2');

      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockReturnValueOnce([task2])
        .mockReturnValueOnce([]);

      // When
      await runAllTasks('/project');

      // Then: Worker pool uses claimNextTasks for fetching more tasks
      expect(mockClaimNextTasks).toHaveBeenCalled();
      expect(mockStatus).toHaveBeenCalledWith('Total', '2');
    });
  });

  describe('parallel execution (concurrency>1)', () => {
    beforeEach(() => {
      mockLoadGlobalConfig.mockReturnValue({
        language: 'en',
        defaultPiece: 'default',
        logLevel: 'info',
        notificationSound: true,
        notificationSoundEvents: { runComplete: true, runAbort: true },
        concurrency: 3,
        taskPollIntervalMs: 500,
      });
    });

    it('should display concurrency info when concurrency > 1', async () => {
      // Given: Tasks available
      const task1 = createTask('task-1');
      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockReturnValueOnce([]);

      // When
      await runAllTasks('/project');

      // Then
      expect(mockInfo).toHaveBeenCalledWith('Concurrency: 3');
    });

    it('should execute tasks using worker pool when concurrency > 1', async () => {
      // Given: 3 tasks available
      const task1 = createTask('task-1');
      const task2 = createTask('task-2');
      const task3 = createTask('task-3');

      mockClaimNextTasks
        .mockReturnValueOnce([task1, task2, task3])
        .mockReturnValueOnce([]);

      // In parallel mode, task start messages go through TaskPrefixWriter → process.stdout.write
      const stdoutChunks: string[] = [];
      const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk: unknown) => {
        stdoutChunks.push(String(chunk));
        return true;
      });

      // When
      await runAllTasks('/project');
      writeSpy.mockRestore();

      // Then: Task names displayed with prefix in stdout
      const allOutput = stdoutChunks.join('');
      expect(allOutput).toContain('[task]');
      expect(allOutput).toContain('=== Task: task-1 ===');
      expect(allOutput).toContain('[task]');
      expect(allOutput).toContain('=== Task: task-2 ===');
      expect(allOutput).toContain('[task]');
      expect(allOutput).toContain('=== Task: task-3 ===');
      expect(mockStatus).toHaveBeenCalledWith('Total', '3');
    });

    it('should fill slots as tasks complete (worker pool behavior)', async () => {
      // Given: 5 tasks, concurrency=3
      // Worker pool should start 3, then fill slots as tasks complete
      const tasks = Array.from({ length: 5 }, (_, i) => createTask(`task-${i + 1}`));

      mockClaimNextTasks
        .mockReturnValueOnce(tasks.slice(0, 3))
        .mockReturnValueOnce(tasks.slice(3, 5))
        .mockReturnValueOnce([]);

      // When
      await runAllTasks('/project');

      // Then: All 5 tasks executed
      expect(mockStatus).toHaveBeenCalledWith('Total', '5');
    });
  });

  describe('default concurrency', () => {
    it('should default to sequential when concurrency is not set', async () => {
      // Given: Config without explicit concurrency (defaults to 1)
      mockLoadGlobalConfig.mockReturnValue({
        language: 'en',
        defaultPiece: 'default',
        logLevel: 'info',
        notificationSound: false,
        concurrency: 1,
        taskPollIntervalMs: 500,
      });

      const task1 = createTask('task-1');
      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockReturnValueOnce([]);

      // When
      await runAllTasks('/project');

      // Then: No concurrency info displayed
      const concurrencyInfoCalls = mockInfo.mock.calls.filter(
        (call) => typeof call[0] === 'string' && call[0].startsWith('Concurrency:')
      );
      expect(concurrencyInfoCalls).toHaveLength(0);
      expect(mockNotifySuccess).not.toHaveBeenCalled();
      expect(mockNotifyError).not.toHaveBeenCalled();
    });
  });

  describe('parallel execution behavior', () => {
    const fakePieceConfig = {
      name: 'default',
      movements: [{ name: 'implement', personaDisplayName: 'coder' }],
      initialMovement: 'implement',
      maxMovements: 10,
    };

    beforeEach(() => {
      mockLoadGlobalConfig.mockReturnValue({
        language: 'en',
        defaultPiece: 'default',
        logLevel: 'info',
        concurrency: 3,
        taskPollIntervalMs: 500,
      });
      // Return a valid piece config so executeTask reaches executePiece
      mockLoadPieceByIdentifier.mockReturnValue(fakePieceConfig as never);
    });

    it('should run tasks concurrently, not sequentially', async () => {
      // Given: 2 tasks with delayed execution to verify concurrency
      const task1 = createTask('slow-1');
      const task2 = createTask('slow-2');

      const executionOrder: string[] = [];

      // Each task takes 50ms — if sequential, total > 100ms; if parallel, total ~50ms
      mockExecutePiece.mockImplementation((_config, task) => {
        executionOrder.push(`start:${task}`);
        return new Promise((resolve) => {
          setTimeout(() => {
            executionOrder.push(`end:${task}`);
            resolve({ success: true });
          }, 50);
        });
      });

      mockClaimNextTasks
        .mockReturnValueOnce([task1, task2])
        .mockReturnValueOnce([]);

      // When
      const startTime = Date.now();
      await runAllTasks('/project');
      const elapsed = Date.now() - startTime;

      // Then: Both tasks started before either completed (concurrent execution)
      expect(executionOrder[0]).toBe('start:Task: slow-1');
      expect(executionOrder[1]).toBe('start:Task: slow-2');
      // Elapsed time should be closer to 50ms than 100ms (allowing margin for CI)
      expect(elapsed).toBeLessThan(150);
    });

    it('should fill slots immediately when a task completes (no batch waiting)', async () => {
      // Given: 3 tasks, concurrency=2, task1 finishes quickly, task2 takes longer
      mockLoadGlobalConfig.mockReturnValue({
        language: 'en',
        defaultPiece: 'default',
        logLevel: 'info',
        concurrency: 2,
        taskPollIntervalMs: 500,
      });

      const task1 = createTask('fast');
      const task2 = createTask('slow');
      const task3 = createTask('after-fast');

      const executionOrder: string[] = [];

      mockExecutePiece.mockImplementation((_config, task) => {
        executionOrder.push(`start:${task}`);
        const delay = (task as string).includes('slow') ? 80 : 20;
        return new Promise((resolve) => {
          setTimeout(() => {
            executionOrder.push(`end:${task}`);
            resolve({ success: true });
          }, delay);
        });
      });

      mockClaimNextTasks
        .mockReturnValueOnce([task1, task2])
        .mockReturnValueOnce([task3])
        .mockReturnValueOnce([]);

      // When
      await runAllTasks('/project');

      // Then: task3 starts before task2 finishes (slot filled immediately)
      const task3StartIdx = executionOrder.indexOf('start:Task: after-fast');
      const task2EndIdx = executionOrder.indexOf('end:Task: slow');
      expect(task3StartIdx).toBeLessThan(task2EndIdx);
      expect(mockStatus).toHaveBeenCalledWith('Total', '3');
    });

    it('should count partial failures correctly', async () => {
      // Given: 3 tasks, 1 fails, 2 succeed
      mockLoadGlobalConfig.mockReturnValue({
        language: 'en',
        defaultPiece: 'default',
        logLevel: 'info',
        notificationSound: true,
        notificationSoundEvents: { runAbort: true },
        concurrency: 3,
        taskPollIntervalMs: 500,
      });

      const task1 = createTask('pass-1');
      const task2 = createTask('fail-1');
      const task3 = createTask('pass-2');

      let callIndex = 0;
      mockExecutePiece.mockImplementation(() => {
        callIndex++;
        // Second call fails
        return Promise.resolve({ success: callIndex !== 2 });
      });

      mockClaimNextTasks
        .mockReturnValueOnce([task1, task2, task3])
        .mockReturnValueOnce([]);

      // When
      await runAllTasks('/project');

      // Then: Correct success/fail counts
      expect(mockStatus).toHaveBeenCalledWith('Total', '3');
      expect(mockStatus).toHaveBeenCalledWith('Success', '2', undefined);
      expect(mockStatus).toHaveBeenCalledWith('Failed', '1', 'red');
      expect(mockNotifySuccess).not.toHaveBeenCalled();
      expect(mockNotifyError).toHaveBeenCalledTimes(1);
    });

    it('should persist failure reason and movement when piece aborts', async () => {
      const task1 = createTask('fail-with-detail');

      mockExecutePiece.mockResolvedValue({
        success: false,
        reason: 'blocked_by_review',
        lastMovement: 'review',
        lastMessage: 'security check failed',
      });
      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockReturnValueOnce([]);

      await runAllTasks('/project');

      expect(mockFailTask).toHaveBeenCalledWith(expect.objectContaining({
        response: 'blocked_by_review',
        failureMovement: 'review',
        failureLastMessage: 'security check failed',
      }));
    });

    it('should pass abortSignal and taskPrefix to executePiece in parallel mode', async () => {
      // Given: One task in parallel mode
      const task1 = createTask('parallel-task');

      mockExecutePiece.mockResolvedValue({ success: true });

      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockReturnValueOnce([]);

      // When
      await runAllTasks('/project');

      // Then: executePiece received abortSignal and taskPrefix options
      expect(mockExecutePiece).toHaveBeenCalledTimes(1);
      const callArgs = mockExecutePiece.mock.calls[0];
      const pieceOptions = callArgs?.[3]; // 4th argument is options
      expect(pieceOptions).toHaveProperty('abortSignal');
      expect(pieceOptions?.abortSignal).toBeInstanceOf(AbortSignal);
      expect(pieceOptions).toHaveProperty('taskPrefix', 'parallel-task');
    });

    it('should pass abortSignal but not taskPrefix in sequential mode', async () => {
      // Given: Sequential mode
      mockLoadGlobalConfig.mockReturnValue({
        language: 'en',
        defaultPiece: 'default',
        logLevel: 'info',
        notificationSound: true,
        notificationSoundEvents: { runComplete: true, runAbort: true },
        concurrency: 1,
        taskPollIntervalMs: 500,
      });

      const task1 = createTask('sequential-task');
      mockExecutePiece.mockResolvedValue({ success: true });
      mockLoadPieceByIdentifier.mockReturnValue(fakePieceConfig as never);

      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockReturnValueOnce([]);

      // When
      await runAllTasks('/project');

      // Then: executePiece should have abortSignal but not taskPrefix
      expect(mockExecutePiece).toHaveBeenCalledTimes(1);
      const callArgs = mockExecutePiece.mock.calls[0];
      const pieceOptions = callArgs?.[3];
      expect(pieceOptions?.abortSignal).toBeInstanceOf(AbortSignal);
      expect(pieceOptions?.taskPrefix).toBeUndefined();
    });

    it('should only notify once at run completion when multiple tasks succeed', async () => {
      mockLoadGlobalConfig.mockReturnValue({
        language: 'en',
        defaultPiece: 'default',
        logLevel: 'info',
        notificationSound: true,
        notificationSoundEvents: { runComplete: true },
        concurrency: 3,
        taskPollIntervalMs: 500,
      });

      const task1 = createTask('task-1');
      const task2 = createTask('task-2');
      const task3 = createTask('task-3');

      mockClaimNextTasks
        .mockReturnValueOnce([task1, task2, task3])
        .mockReturnValueOnce([]);

      await runAllTasks('/project');

      expect(mockNotifySuccess).toHaveBeenCalledTimes(1);
      expect(mockNotifyError).not.toHaveBeenCalled();
    });

    it('should not notify run completion when runComplete is explicitly false', async () => {
      mockLoadGlobalConfig.mockReturnValue({
        language: 'en',
        defaultPiece: 'default',
        logLevel: 'info',
        notificationSound: true,
        notificationSoundEvents: { runComplete: false },
        concurrency: 1,
        taskPollIntervalMs: 500,
      });

      const task1 = createTask('task-1');
      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockReturnValueOnce([]);

      await runAllTasks('/project');

      expect(mockNotifySuccess).not.toHaveBeenCalled();
      expect(mockNotifyError).not.toHaveBeenCalled();
    });

    it('should notify run completion by default when notification_sound_events is not set', async () => {
      mockLoadGlobalConfig.mockReturnValue({
        language: 'en',
        defaultPiece: 'default',
        logLevel: 'info',
        notificationSound: true,
        concurrency: 1,
        taskPollIntervalMs: 500,
      });

      const task1 = createTask('task-1');
      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockReturnValueOnce([]);

      await runAllTasks('/project');

      expect(mockNotifySuccess).toHaveBeenCalledTimes(1);
      expect(mockNotifySuccess).toHaveBeenCalledWith('TAKT', 'run.notifyComplete');
      expect(mockNotifyError).not.toHaveBeenCalled();
    });

    it('should notify run abort by default when notification_sound_events is not set', async () => {
      mockLoadGlobalConfig.mockReturnValue({
        language: 'en',
        defaultPiece: 'default',
        logLevel: 'info',
        notificationSound: true,
        concurrency: 1,
        taskPollIntervalMs: 500,
      });

      const task1 = createTask('task-1');
      mockExecutePiece.mockResolvedValueOnce({ success: false, reason: 'failed' });
      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockReturnValueOnce([]);

      await runAllTasks('/project');

      expect(mockNotifySuccess).not.toHaveBeenCalled();
      expect(mockNotifyError).toHaveBeenCalledTimes(1);
      expect(mockNotifyError).toHaveBeenCalledWith('TAKT', 'run.notifyAbort');
    });

    it('should not notify run abort when runAbort is explicitly false', async () => {
      mockLoadGlobalConfig.mockReturnValue({
        language: 'en',
        defaultPiece: 'default',
        logLevel: 'info',
        notificationSound: true,
        notificationSoundEvents: { runAbort: false },
        concurrency: 1,
        taskPollIntervalMs: 500,
      });

      const task1 = createTask('task-1');
      mockExecutePiece.mockResolvedValueOnce({ success: false, reason: 'failed' });
      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockReturnValueOnce([]);

      await runAllTasks('/project');

      expect(mockNotifySuccess).not.toHaveBeenCalled();
      expect(mockNotifyError).not.toHaveBeenCalled();
    });

    it('should notify run abort and rethrow when worker pool throws', async () => {
      mockLoadGlobalConfig.mockReturnValue({
        language: 'en',
        defaultPiece: 'default',
        logLevel: 'info',
        notificationSound: true,
        notificationSoundEvents: { runAbort: true },
        concurrency: 1,
        taskPollIntervalMs: 500,
      });

      const task1 = createTask('task-1');
      const poolError = new Error('worker pool crashed');

      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockImplementationOnce(() => {
          throw poolError;
        });

      await expect(runAllTasks('/project')).rejects.toThrow('worker pool crashed');
      expect(mockNotifyError).toHaveBeenCalledTimes(1);
      expect(mockNotifyError).toHaveBeenCalledWith('TAKT', 'run.notifyAbort');
    });
  });

  describe('Slack webhook notification', () => {
    const webhookUrl = 'https://hooks.slack.com/services/T00/B00/xxx';
    const fakePieceConfig = {
      name: 'default',
      movements: [{ name: 'implement', personaDisplayName: 'coder' }],
      initialMovement: 'implement',
      maxMovements: 10,
    };

    beforeEach(() => {
      mockLoadGlobalConfig.mockReturnValue({
        language: 'en',
        defaultPiece: 'default',
        logLevel: 'info',
        concurrency: 1,
        taskPollIntervalMs: 500,
      });
      mockLoadPieceByIdentifier.mockReturnValue(fakePieceConfig as never);
    });

    it('should send Slack notification on success when webhook URL is set', async () => {
      // Given
      mockGetSlackWebhookUrl.mockReturnValue(webhookUrl);
      const task1 = createTask('task-1');
      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockReturnValueOnce([]);

      // When
      await runAllTasks('/project');

      // Then
      expect(mockSendSlackNotification).toHaveBeenCalledOnce();
      expect(mockSendSlackNotification).toHaveBeenCalledWith(
        webhookUrl,
        'TAKT Run complete: 1 tasks succeeded',
      );
    });

    it('should send Slack notification on failure when webhook URL is set', async () => {
      // Given
      mockGetSlackWebhookUrl.mockReturnValue(webhookUrl);
      const task1 = createTask('task-1');
      mockExecutePiece.mockResolvedValueOnce({ success: false, reason: 'failed' });
      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockReturnValueOnce([]);

      // When
      await runAllTasks('/project');

      // Then
      expect(mockSendSlackNotification).toHaveBeenCalledOnce();
      expect(mockSendSlackNotification).toHaveBeenCalledWith(
        webhookUrl,
        'TAKT Run finished with errors: 1 failed out of 1 tasks',
      );
    });

    it('should send Slack notification on exception when webhook URL is set', async () => {
      // Given
      mockGetSlackWebhookUrl.mockReturnValue(webhookUrl);
      const task1 = createTask('task-1');
      const poolError = new Error('worker pool crashed');
      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockImplementationOnce(() => {
          throw poolError;
        });

      // When / Then
      await expect(runAllTasks('/project')).rejects.toThrow('worker pool crashed');
      expect(mockSendSlackNotification).toHaveBeenCalledOnce();
      expect(mockSendSlackNotification).toHaveBeenCalledWith(
        webhookUrl,
        'TAKT Run error: worker pool crashed',
      );
    });

    it('should not send Slack notification when webhook URL is not set', async () => {
      // Given
      mockGetSlackWebhookUrl.mockReturnValue(undefined);
      const task1 = createTask('task-1');
      mockClaimNextTasks
        .mockReturnValueOnce([task1])
        .mockReturnValueOnce([]);

      // When
      await runAllTasks('/project');

      // Then
      expect(mockSendSlackNotification).not.toHaveBeenCalled();
    });
  });
});
