Skip to content

Add unit tests for parallel-executor module#252

Closed
nikolasdehor wants to merge 1 commit intoSynkraAI:mainfrom
nikolasdehor:test/parallel-executor-coverage
Closed

Add unit tests for parallel-executor module#252
nikolasdehor wants to merge 1 commit intoSynkraAI:mainfrom
nikolasdehor:test/parallel-executor-coverage

Conversation

@nikolasdehor
Copy link
Contributor

@nikolasdehor nikolasdehor commented Feb 18, 2026

Summary

  • Add 36 unit tests for the parallel-executor orchestration module
  • Cover concurrent execution, concurrency limits, task lifecycle, status tracking, and summary statistics
  • Chalk mocked to prevent console formatting in test output

Test Coverage

Area Tests Key Scenarios
constructor 2 Default concurrency, empty task map
executeParallel 12 Success, failures, custom concurrency, step key, logging
getStatus 5 Duration calculation, copy isolation
hasRunningTasks 3 Running, completed, empty
waitForCompletion 2 Immediate resolve, timeout
cancelAll 3 Mark cancelled, preserve others
clear 1 Remove all tasks
setMaxConcurrency 3 Clamping 1-10
getSummary 5 Counts, avg duration, cancelled
Total 36

Test plan

  • All 36 tests pass
  • No external dependencies needed
  • Chalk properly mocked

Summary by CodeRabbit

  • Tests
    • Added comprehensive unit test suite for the ParallelExecutor module, covering initialization, execution, status tracking, task cancellation, and error handling scenarios.

Closes #307

Copilot AI review requested due to automatic review settings February 18, 2026 19:45
@vercel
Copy link

vercel bot commented Feb 18, 2026

@nikolasdehor is attempting to deploy a commit to the Pedro Valério Lopez's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

Walkthrough

A comprehensive unit test suite for the ParallelExecutor module is introduced, covering constructor initialization, execution behavior, task tracking, status reporting, concurrency limits, cancellation, and summary aggregation across multiple test cases with mocked console output.

Changes

Cohort / File(s) Summary
ParallelExecutor Unit Tests
tests/core/orchestration/parallel-executor.test.js
Comprehensive test suite covering constructor state, executeParallel flow with success and error scenarios, task status and duration tracking, concurrency constraints, cancellation behavior, task clearing, status snapshots, and summary aggregation logic.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related issues

  • #251: Adds comprehensive unit tests for the same ParallelExecutor module covering identical behaviors and public methods, directly addressing the same testing objectives.

Suggested labels

core, tests

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately describes the main change: adding unit tests for the parallel-executor module, which aligns with the PR's objective of introducing comprehensive test coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Jest unit test suite for the .aios-core/core/orchestration/parallel-executor module to validate task lifecycle/status tracking, summary reporting, cancellation, and logging behavior.

Changes:

  • Introduces ParallelExecutor unit tests covering executeParallel, getStatus, hasRunningTasks, waitForCompletion, cancelAll, clear, setMaxConcurrency, and getSummary.
  • Mocks chalk to avoid color formatting in test output.
  • Adds assertions around status transitions, error capture, and summary statistics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

jest.spyOn(console, 'log').mockImplementation();
executor = new ParallelExecutor();
});

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console.log is spied/mocked in beforeEach but never restored. This can make debugging harder and diverges from the pattern used in other tests (e.g. restoring spies in afterEach). Add an afterEach that calls mockRestore() (or use jest.restoreAllMocks()) so the original console method is always put back.

Suggested change
afterEach(() => {
jest.restoreAllMocks();
});

Copilot uses AI. Check for mistakes.
Comment on lines +390 to +398
test('ignores cancelled tasks in counts', () => {
executor.runningTasks.set('a', { status: 'cancelled' });

const summary = executor.getSummary();
expect(summary.total).toBe(1);
expect(summary.completed).toBe(0);
expect(summary.failed).toBe(0);
expect(summary.running).toBe(0);
});
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test name says "ignores cancelled tasks in counts", but the assertions still expect summary.total to include the cancelled task. Either update the test name to reflect the behavior (e.g. cancelled excluded from completed/failed/running only) or adjust expectations if cancelled should be excluded entirely.

Copilot uses AI. Check for mistakes.

expect(executePhase).toHaveBeenCalledTimes(1);
});

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only verifies that maxConcurrency can be passed, but it doesn't assert that the concurrency limit is actually enforced (i.e., that executePhase isn't running more than maxConcurrency tasks at once). Add a test that tracks the peak number of simultaneously-running executePhase calls (using a barrier/pending promise) and asserts it never exceeds the configured limit.

Suggested change
test('does not exceed configured maxConcurrency', async () => {
const maxConcurrency = 2;
const phases = [
{ phase: 'p1' },
{ phase: 'p2' },
{ phase: 'p3' },
{ phase: 'p4' },
{ phase: 'p5' },
];
let currentRunning = 0;
let peakRunning = 0;
const releaseFns = [];
const executePhase = jest.fn().mockImplementation(() => {
currentRunning += 1;
if (currentRunning > peakRunning) {
peakRunning = currentRunning;
}
let resolveBarrier;
const barrier = new Promise((resolve) => {
resolveBarrier = resolve;
});
// Store a function that, when called, will mark this task as finished.
releaseFns.push(() => {
currentRunning -= 1;
resolveBarrier();
});
return barrier;
});
const executionPromise = executor.executeParallel(phases, executePhase, {
maxConcurrency,
});
// Wait until at least maxConcurrency tasks have started.
while (releaseFns.length < Math.min(maxConcurrency, phases.length)) {
// Yield to the event loop to allow more tasks to start.
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => setTimeout(resolve, 0));
}
expect(peakRunning).toBeLessThanOrEqual(maxConcurrency);
// Allow all running tasks to complete.
releaseFns.forEach((release) => release());
const result = await executionPromise;
expect(executePhase).toHaveBeenCalledTimes(phases.length);
expect(result.results).toHaveLength(phases.length);
expect(result.errors).toHaveLength(0);
});

Copilot uses AI. Check for mistakes.
Comment on lines +264 to +270
test('throws on timeout', async () => {
executor.runningTasks.set('stuck', { status: 'running' });

await expect(executor.waitForCompletion(50)).rejects.toThrow(
'Timeout waiting for parallel tasks to complete'
);
});
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout test uses real timers, and waitForCompletion sleeps in 100ms increments, so this test will incur a real delay and can be slightly flaky under load. Consider using jest.useFakeTimers() / advanceTimersByTime() here (as done in other timeout tests in the repo) to make it fast and deterministic.

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
tests/core/orchestration/parallel-executor.test.js (3)

19-23: Add afterEach(() => jest.restoreAllMocks()) to properly tear down the console.log spy.

jest.resetAllMocks() resets call counts and implementations but does not restore the original console.log. Without an explicit restoreAllMocks() call, the spy persists for the lifetime of the test file, which can cause unexpected side effects if test ordering changes or the module cache is shared.

♻️ Proposed fix
  beforeEach(() => {
    jest.resetAllMocks();
    jest.spyOn(console, 'log').mockImplementation();
    executor = new ParallelExecutor();
  });
+
+ afterEach(() => {
+   jest.restoreAllMocks();
+ });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/orchestration/parallel-executor.test.js` around lines 19 - 23, The
test sets a console.log spy in the beforeEach (jest.spyOn(console, 'log') in the
ParallelExecutor tests) but never restores it; add an afterEach that calls
jest.restoreAllMocks() to properly tear down the console.log spy after each test
(so add an afterEach(() => jest.restoreAllMocks()) alongside the existing
beforeEach where executor = new ParallelExecutor()).

390-398: Misleading test name — cancelled tasks are still reflected in total.

The test name says "ignores cancelled tasks in counts", yet the assertion expect(summary.total).toBe(1) confirms they are counted in total. The description should clarify the actual intent: cancelled tasks are excluded from the per-status buckets but still contribute to total.

♻️ Suggested rename
-    test('ignores cancelled tasks in counts', () => {
+    test('excludes cancelled tasks from per-status buckets but includes them in total', () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/orchestration/parallel-executor.test.js` around lines 390 - 398,
Rename the test so the title accurately reflects that cancelled tasks are
counted in summary.total but excluded from per-status buckets; update the test
name for the case in the test that manipulates executor.runningTasks and calls
executor.getSummary() (currently using a 'cancelled' status) to something like
"counts cancelled tasks in total but ignores them in per-status counts" so the
test intent aligns with the assertions on summary.total, summary.completed,
summary.failed, and summary.running.

63-65: Mock-order assumption is fragile for parallel execution.

mockResolvedValueOnce / mockRejectedValueOnce assign behaviour by invocation index, implicitly relying on the executor calling executePhase for 'good' before 'bad'. For a truly parallel executor this depends on the internal iteration order remaining stable. Consider using a named-argument match instead, so the correct outcome is tied to the phase regardless of dispatch order.

♻️ More resilient alternative
-      const executePhase = jest.fn()
-        .mockResolvedValueOnce({ ok: true })
-        .mockRejectedValueOnce(new Error('Phase failed'));
+      const executePhase = jest.fn().mockImplementation((phase) => {
+        if (phase.phase === 'bad') return Promise.reject(new Error('Phase failed'));
+        return Promise.resolve({ ok: true });
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/orchestration/parallel-executor.test.js` around lines 63 - 65, The
test's use of mockResolvedValueOnce/mockRejectedValueOnce for executePhase is
fragile because it depends on invocation order; replace it with a
mockImplementation that inspects the phase argument (e.g., phase.id or
phase.name used in the test) and returns Promise.resolve({ok:true}) for the
'good' phase and Promise.reject(new Error('Phase failed')) for the 'bad' phase
so behavior is tied to the phase identity rather than call index; update the
executePhase mock in parallel-executor.test.js to use this argument-based
implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/core/orchestration/parallel-executor.test.js`:
- Around line 264-270: The timeout test is flaky because it relies on real
timers; change the test that calls executor.waitForCompletion(50) to use Jest
fake timers: call jest.useFakeTimers() before setting executor.runningTasks and
invoking executor.waitForCompletion, trigger the timeout by advancing timers
synchronously with jest.advanceTimersByTime(50) (or slightly beyond), assert the
promise rejects as before, and finally restore timers with jest.useRealTimers()
to avoid polluting other tests; locate this logic around the test named 'throws
on timeout' and the call to executor.waitForCompletion and
executor.runningTasks.
- Around line 87-94: The test is ineffective because a single phase cannot
exercise maxConcurrency; modify the test for executeParallel to create more
phases than the provided maxConcurrency (e.g., 10 phases with maxConcurrency: 5)
and replace the simple mock executePhase with a controlled async function that
tracks current concurrent invocations (increment a counter at start, await a
controllable delay, then decrement) so you can assert the peak concurrent
counter never exceeds the configured limit; use the executeParallel and
executePhase identifiers to locate and update the test and add assertions that
peakConcurrent <= 5 and executePhase was called for each phase.

---

Nitpick comments:
In `@tests/core/orchestration/parallel-executor.test.js`:
- Around line 19-23: The test sets a console.log spy in the beforeEach
(jest.spyOn(console, 'log') in the ParallelExecutor tests) but never restores
it; add an afterEach that calls jest.restoreAllMocks() to properly tear down the
console.log spy after each test (so add an afterEach(() =>
jest.restoreAllMocks()) alongside the existing beforeEach where executor = new
ParallelExecutor()).
- Around line 390-398: Rename the test so the title accurately reflects that
cancelled tasks are counted in summary.total but excluded from per-status
buckets; update the test name for the case in the test that manipulates
executor.runningTasks and calls executor.getSummary() (currently using a
'cancelled' status) to something like "counts cancelled tasks in total but
ignores them in per-status counts" so the test intent aligns with the assertions
on summary.total, summary.completed, summary.failed, and summary.running.
- Around line 63-65: The test's use of
mockResolvedValueOnce/mockRejectedValueOnce for executePhase is fragile because
it depends on invocation order; replace it with a mockImplementation that
inspects the phase argument (e.g., phase.id or phase.name used in the test) and
returns Promise.resolve({ok:true}) for the 'good' phase and Promise.reject(new
Error('Phase failed')) for the 'bad' phase so behavior is tied to the phase
identity rather than call index; update the executePhase mock in
parallel-executor.test.js to use this argument-based implementation.

Comment on lines +87 to +94
test('uses custom maxConcurrency from options', async () => {
const phases = [{ phase: 'a' }];
const executePhase = jest.fn().mockResolvedValue({});

await executor.executeParallel(phases, executePhase, { maxConcurrency: 5 });

expect(executePhase).toHaveBeenCalledTimes(1);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Test doesn't actually verify custom concurrency is respected.

With a single-phase input, executePhase will always be called exactly once regardless of what maxConcurrency is set to. This test provides no signal about whether the option is wired up or enforced. To exercise the concurrency cap, you need more phases than the limit and a way to observe that at most N are running simultaneously.

♻️ More meaningful concurrency test
-    test('uses custom maxConcurrency from options', async () => {
-      const phases = [{ phase: 'a' }];
-      const executePhase = jest.fn().mockResolvedValue({});
-
-      await executor.executeParallel(phases, executePhase, { maxConcurrency: 5 });
-
-      expect(executePhase).toHaveBeenCalledTimes(1);
-    });
+    test('respects custom maxConcurrency — never exceeds the cap', async () => {
+      const concurrencyLimit = 2;
+      let currentlyRunning = 0;
+      let peakConcurrency = 0;
+      const phases = [{ phase: 'a' }, { phase: 'b' }, { phase: 'c' }, { phase: 'd' }];
+
+      const executePhase = jest.fn().mockImplementation(async () => {
+        currentlyRunning++;
+        peakConcurrency = Math.max(peakConcurrency, currentlyRunning);
+        await new Promise((r) => setImmediate(r));
+        currentlyRunning--;
+        return {};
+      });
+
+      await executor.executeParallel(phases, executePhase, { maxConcurrency: concurrencyLimit });
+
+      expect(executePhase).toHaveBeenCalledTimes(4);
+      expect(peakConcurrency).toBeLessThanOrEqual(concurrencyLimit);
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test('uses custom maxConcurrency from options', async () => {
const phases = [{ phase: 'a' }];
const executePhase = jest.fn().mockResolvedValue({});
await executor.executeParallel(phases, executePhase, { maxConcurrency: 5 });
expect(executePhase).toHaveBeenCalledTimes(1);
});
test('respects custom maxConcurrency — never exceeds the cap', async () => {
const concurrencyLimit = 2;
let currentlyRunning = 0;
let peakConcurrency = 0;
const phases = [{ phase: 'a' }, { phase: 'b' }, { phase: 'c' }, { phase: 'd' }];
const executePhase = jest.fn().mockImplementation(async () => {
currentlyRunning++;
peakConcurrency = Math.max(peakConcurrency, currentlyRunning);
await new Promise((r) => setImmediate(r));
currentlyRunning--;
return {};
});
await executor.executeParallel(phases, executePhase, { maxConcurrency: concurrencyLimit });
expect(executePhase).toHaveBeenCalledTimes(4);
expect(peakConcurrency).toBeLessThanOrEqual(concurrencyLimit);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/orchestration/parallel-executor.test.js` around lines 87 - 94, The
test is ineffective because a single phase cannot exercise maxConcurrency;
modify the test for executeParallel to create more phases than the provided
maxConcurrency (e.g., 10 phases with maxConcurrency: 5) and replace the simple
mock executePhase with a controlled async function that tracks current
concurrent invocations (increment a counter at start, await a controllable
delay, then decrement) so you can assert the peak concurrent counter never
exceeds the configured limit; use the executeParallel and executePhase
identifiers to locate and update the test and add assertions that peakConcurrent
<= 5 and executePhase was called for each phase.

Comment on lines +264 to +270
test('throws on timeout', async () => {
executor.runningTasks.set('stuck', { status: 'running' });

await expect(executor.waitForCompletion(50)).rejects.toThrow(
'Timeout waiting for parallel tasks to complete'
);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Real-timer timeout test is CI-flaky — use fake timers.

A hard 50 ms wall-clock wait in CI is a classic source of intermittent failures: slow runners can exhaust the timeout before the promise chain is set up, or the OS scheduler introduces jitter that makes the timing non-deterministic. Use jest.useFakeTimers() to advance time synchronously.

♻️ Deterministic rewrite with fake timers
-    test('throws on timeout', async () => {
-      executor.runningTasks.set('stuck', { status: 'running' });
-
-      await expect(executor.waitForCompletion(50)).rejects.toThrow(
-        'Timeout waiting for parallel tasks to complete'
-      );
-    });
+    test('throws on timeout', async () => {
+      jest.useFakeTimers();
+      executor.runningTasks.set('stuck', { status: 'running' });
+
+      const promise = executor.waitForCompletion(50);
+      jest.advanceTimersByTime(51);
+
+      await expect(promise).rejects.toThrow(
+        'Timeout waiting for parallel tasks to complete'
+      );
+      jest.useRealTimers();
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/orchestration/parallel-executor.test.js` around lines 264 - 270,
The timeout test is flaky because it relies on real timers; change the test that
calls executor.waitForCompletion(50) to use Jest fake timers: call
jest.useFakeTimers() before setting executor.runningTasks and invoking
executor.waitForCompletion, trigger the timeout by advancing timers
synchronously with jest.advanceTimersByTime(50) (or slightly beyond), assert the
promise rejects as before, and finally restore timers with jest.useRealTimers()
to avoid polluting other tests; locate this logic around the test named 'throws
on timeout' and the call to executor.waitForCompletion and
executor.runningTasks.

@nikolasdehor
Copy link
Contributor Author

Consolidated into #426

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add unit tests for parallel-executor module

2 participants