Skip to content
Back to skills

Async Ordering Tests

ASecurity

Test async ordering - event-loop / queue / channel ordering assertions, JS Promise microtask vs macrotask ordering, Python `asyncio.gather` vs `asyncio.wait_for` semantics, Go goroutine + channel happens-before relationships, async/await re-entrancy. Use deterministic schedulers (sinon fake timers, asyncio test mode) to remove run-to-run variance. Use when a callback fires twice, a later response overwrites an earlier one, or a cancelled parent task leaves a child still running - bugs where c...

  • 10 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 12, 2026
testingpythongobashnodetesting

Security analysis

A100/100

Pro scans all 8 files and shows the line behind each finding

Scanned October 12, 2026

npx -y skills add testland/qa --skill async-ordering-tests --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Async Ordering Tests?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Async Ordering Tests
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/testland-async-ordering-tests/badge)](https://www.skillsdirectory.com/skills/testland-async-ordering-tests)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: async-ordering-tests
description: "Test async ordering - event-loop / queue / channel ordering assertions, JS Promise microtask vs macrotask ordering, Python `asyncio.gather` vs `asyncio.wait_for` semantics, Go goroutine + channel happens-before relationships, async/await re-entrancy. Use deterministic schedulers (sinon fake timers, asyncio test mode) to remove run-to-run variance. Use when a callback fires twice, a later response overwrites an earlier one, or a cancelled parent task leaves a child still running - bugs where completion order, not shared memory, is the defect."
metadata:
  keywords: "async-ordering, promises, asyncio, happens-before, event-loop"
  version: "1.2.14"
---

# async-ordering-tests

Async ordering bugs ("user A's update overwrote user B's" / "callback
fired twice" / "promise resolved before fetch returned") look like
race conditions but operate at the runtime level (event loop /
scheduler). Tests must control the scheduler to be deterministic.

## When to use

- UI updates that depend on multiple in-flight requests resolving
  in the right order.
- Server-side: parallel I/O whose completion order affects the
  final state.
- Callback-heavy code where re-entrancy (sync vs async dispatch)
  matters.

## Step 1 - JS Promise microtask vs macrotask

```ts
// Microtasks (Promise callbacks) drain BEFORE next macrotask (setTimeout)
test('microtasks drain before setTimeout', async () => {
  const order: string[] = [];

  setTimeout(() => order.push('setTimeout'), 0);
  Promise.resolve().then(() => order.push('promise'));

  // Wait for both to fire
  await new Promise(r => setTimeout(r, 0));

  expect(order).toEqual(['promise', 'setTimeout']);
});
```

Critical: promise resolution callbacks run in the microtask queue,
which drains *between* macrotasks. Misunderstanding this causes
"why did this fire twice" bugs.

## Step 2 - Use deterministic timers (Sinon / Vitest fake)

```ts
import { vi } from 'vitest';

test('debounced search fires once after 300ms', () => {
  vi.useFakeTimers();
  const search = vi.fn();
  const debounced = debounce(search, 300);

  debounced('a');
  debounced('ab');
  debounced('abc');

  vi.advanceTimersByTime(299);
  expect(search).not.toHaveBeenCalled();

  vi.advanceTimersByTime(1);
  expect(search).toHaveBeenCalledOnce();
  expect(search).toHaveBeenCalledWith('abc');
});
```

Real timers + sleep = flake. Fake timers = deterministic.

## Step 3 - `Promise.all` vs sequential await

```ts
test('parallel fetch starts both before either completes', async () => {
  const order: string[] = [];

  function trackedFetch(name: string) {
    order.push(`start-${name}`);
    return new Promise<string>((resolve) =>
      setTimeout(() => {
        order.push(`done-${name}`);
        resolve(name);
      }, 50)
    );
  }

  await Promise.all([trackedFetch('A'), trackedFetch('B')]);

  // Both started before either finished
  expect(order.indexOf('done-A')).toBeGreaterThan(order.indexOf('start-B'));
});
```

vs sequential:

```ts
test('sequential await runs A fully before B starts', async () => {
  const order: string[] = [];
  await trackedFetch('A');
  await trackedFetch('B');

  expect(order).toEqual(['start-A', 'done-A', 'start-B', 'done-B']);
});
```

Test the actual model your code uses, especially when "parallelize"
is an intentional optimization.

## Step 4 - Python asyncio ordering

```python
import asyncio

async def test_gather_concurrency_with_first_completed():
    started = []
    finished = []

    async def task(name, delay):
        started.append(name)
        await asyncio.sleep(delay)
        finished.append(name)

    await asyncio.gather(task("A", 0.05), task("B", 0.01))

    # Both started before either finished
    assert started == ["A", "B"]
    # B finishes first (shorter sleep)
    assert finished == ["B", "A"]
```

For race-style: use `asyncio.wait(..., return_when=FIRST_COMPLETED)`
to test "whoever finishes first wins" semantics.

## Step 5 - Async re-entrancy

A callback fires while another callback is still running. Tests:

```ts
test('handler not invoked while previous invocation in progress', async () => {
  const calls: string[] = [];
  let inProgress = false;

  const handler = async (msg: string) => {
    calls.push(`enter-${msg}`);
    if (inProgress) {
      throw new Error('Re-entrant call detected');
    }
    inProgress = true;
    await new Promise(r => setTimeout(r, 10));
    inProgress = false;
    calls.push(`exit-${msg}`);
  };

  const queue = createSerializingQueue(handler);
  queue.enqueue('a');
  queue.enqueue('b');

  await queue.drain();
  expect(calls).toEqual(['enter-a', 'exit-a', 'enter-b', 'exit-b']);
});
```

Test guards: queue serializes enqueued work even if event loop
allows interleaving.

## Step 6 - Go channel happens-before

```go
func TestChannelHappensBefore(t *testing.T) {
    var x int
    done := make(chan struct{})

    go func() {
        x = 42        // (1)
        done <- struct{}{}  // (2)
    }()

    <-done           // (3)
    if x != 42 {     // (4) - guaranteed by happens-before
        t.Fatalf("expected 42, got %d", x)
    }
}
```

Go memory model: (1) happens-before (2); (2) happens-before (3);
(3) happens-before (4). Without channel sync, (4) could see 0 even
though (1) ran in the goroutine.

For tests, run with `-race`:

```bash
go test -race ./...
```

## Step 7 - Backpressure / queue overflow

```python
async def test_queue_drops_when_full():
    queue = asyncio.Queue(maxsize=3)
    dropped = []

    async def producer():
        for i in range(10):
            try:
                queue.put_nowait(i)
            except asyncio.QueueFull:
                dropped.append(i)

    await producer()
    assert len(dropped) == 7
```

Bounded queues + drop-on-full = silent data loss. Test the policy
explicitly.

## Step 8 - Cancellation propagation

```python
async def test_cancellation_propagates_to_children():
    child_started = asyncio.Event()
    child_cancelled = asyncio.Event()

    async def child():
        try:
            child_started.set()
            await asyncio.sleep(60)
        except asyncio.CancelledError:
            child_cancelled.set()
            raise

    async def parent():
        await child()

    parent_task = asyncio.create_task(parent())
    await child_started.wait()

    parent_task.cancel()
    with pytest.raises(asyncio.CancelledError):
        await parent_task

    assert child_cancelled.is_set()
```

Common bug: parent cancelled, child task leaks (continues running).

## Anti-patterns

| Anti-pattern | Why it fails | Fix |
|---|---|---|
| `await sleep(100)` to "let things settle" | Flake on slow CI | Fake timers / explicit waits (Step 2) |
| Test `Promise.all` order assuming sequential | Non-deterministic interleave | Test model explicitly (Step 3) |
| Skip cancellation tests | Cancelled tasks leak forever | Cancellation propagation test (Step 8) |
| Use real timers + `setTimeout(0)` for "next tick" | Race with microtask drain | `queueMicrotask()` or `Promise.resolve()` |
| Test only "happy path" with single task | Concurrency bugs hide | Multi-task scenarios (Step 4) |

## Limitations

- Microtask vs macrotask semantics differ across runtimes (Node
  `process.nextTick` is a third queue between).
- asyncio's behavior changed across Python versions (3.10's
  task-cancellation handling differs from 3.7).
- Go's `-race` finds data races, not all happens-before violations.
- Some libraries (RxJS) layer their own scheduling over the
  runtime - test their semantics, not just the runtime's.

## References

- ECMAScript spec - runtime job semantics; consult tc39.es/ecma262
- Python asyncio docs - docs.python.org/3/library/asyncio.html
- Go memory model - go.dev/ref/mem
- `race-condition-test-author`,
  `deadlock-detection-harness` - 
  sister concurrency skills

Files in this skill

  • .tessl-plugin/plugin.json684 B
  • SKILL.md7.7 KB
  • evals/cancellation-propagation/criteria.json3.1 KB
  • evals/cancellation-propagation/task.md2.6 KB
  • evals/microtask-before-macrotask/criteria.json3.2 KB
  • evals/microtask-before-macrotask/task.md2.3 KB
  • evals/stale-response-overwrite/criteria.json3.1 KB
  • evals/stale-response-overwrite/task.md2.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.