Skip to content

Latest commit

 

History

History
294 lines (226 loc) · 6.09 KB

File metadata and controls

294 lines (226 loc) · 6.09 KB

section-tests Migration For Agents

This document explains how to migrate old section-tests suites that use the legacy section(...) API into the new atomic test(...) model.

The target reader is an AI agent doing large-scale migration work. The guidance is explicit and pattern-based so it can be applied consistently across many files.

Core Rule

Share code, not runtime state.

The new model assumes:

  • each test is independent
  • each test may optionally define setup -> run -> teardown
  • setup returns data for one test execution only
  • reusable setup belongs in helper functions, not in shared live fixture instances
  • tests are parallel by default
  • timeouts abort the test context and may still attempt bounded teardown before the worker is terminated

Old To New Mapping

section('name', callback)

Old:

section('users', (section) => {
  section.test('create', async () => {
    // ...
  });
});

New:

import { defineTests, suite, test } from 'section-tests';

export default defineTests(
  suite(
    'users',
    test('create', async () => {
      // ...
    }),
  ),
);

Notes:

  • nested section(...) trees become nested suite(...) trees
  • suite nesting is for organization and reporting only
  • do not rely on suite order for behavior

section.test('name', fn)

Old:

section.test('create user', async () => {
  const result = await createUser();
  assert.ok(result);
});

New:

test('create user', async () => {
  const result = await createUser();
  assert.ok(result);
});

If there is no setup or teardown, use the short form.

section.setup(...) and section.destroy(...)

Old:

section.setup(async () => {
  await connectDb();
});

section.destroy(async () => {
  await closeDb();
});

New:

async function createDb() {
  const db = await connectDb();
  return db;
}

async function disposeDb(db) {
  await closeDb(db);
}

test('example', {
  async setup() {
    return await createDb();
  },
  async run(db) {
    // ...
  },
  async teardown(db) {
    await disposeDb(db);
  },
});

Important:

  • do not convert one global setup into one global shared runtime object unless the code is truly serial-only
  • instead, extract reusable helper functions and call them per test
  • write setup/run/teardown helpers so they can stop cooperatively when the provided AbortSignal is aborted

Common Migration Patterns

1. Shared Setup Code

If many old tests use the same setup flow, extract a helper:

function dbTest(name, params, run) {
  return test(name, {
    async setup() {
      return await createUsersDb(params);
    },
    async run(db, context) {
      await run(db, context);
    },
    async teardown(db) {
      await disposeUsersDb(db);
    },
  });
}

Use that helper instead of inventing runtime-sharing semantics.

2. File-Order Coupling

Old:

  • 000.setup.js registers reporter or setup
  • 999.main.js assumes earlier files already ran

New:

  • each file must export self-contained tests or suites
  • no file may depend on another file having executed first
  • move shared logic into imported helpers

3. Nested Sequential Flows

Old:

section('workflow', (section) => {
  section.test('step 1', async () => { ... });
  section.test('step 2', async () => { ... });
  section.test('step 3', async () => { ... });
});

If later steps depend on earlier steps, do not keep them as separate parallel tests.

Rewrite as either:

  • independent tests with their own setup, or
  • one explicit scenario test

Example:

test('workflow scenario', {
  async setup() {
    return await createWorkflowState();
  },
  async run(state) {
    await step1(state);
    await step2(state);
    await step3(state);
  },
  async teardown(state) {
    await disposeWorkflowState(state);
  },
});

4. Inline Logging

Old:

section.info('created user');
section.warn('slow path');

New:

test('example', async (t) => {
  t.info('created user');
  t.warn('slow path');
});

5. Timeout-Aware Helpers

Helpers used from setup, run, or teardown should be cancellation-aware whenever possible.

Prefer:

test('example', {
  async run(_setup, context) {
    await doWork({ signal: context.signal });
  },
});

Avoid migration patterns that create long-running work with no way to stop, unless the test is truly exercising timeout behavior itself.

Or with setup:

test('example', {
  async setup(t) {
    t.info('preparing');
    return await createThing();
  },
  async run(thing, t) {
    t.success('running');
  },
});

When To Keep Something Serial

Prefer parallel tests. Only keep a test serial if it truly touches a global or exclusive resource that cannot be isolated.

Use serial(...) for cases like:

  • one real port that cannot be changed
  • one external account with rate limits or global state
  • one process-wide singleton that cannot be refactored yet

Do not mark tests serial just because old code used a shared setup tree.

Migration Checklist

For every old file:

  1. Remove implicit file-order assumptions.
  2. Convert section(...) nesting to suite(...) only when it helps naming/reporting.
  3. Convert section.test(...) to test(...).
  4. Replace section.setup(...) / section.destroy(...) with reusable helper functions plus per-test lifecycle hooks.
  5. Ensure every test can run alone.
  6. Use serial(...) only for true exclusivity.
  7. Make helpers cancellation-aware if they may run for a long time.
  8. Preserve test names where possible so reporter output remains recognizable.

Preferred Output Shape

A migrated file should usually look like this:

import { defineTests, suite, test } from 'section-tests';

async function createThing() {
  // ...
}

async function disposeThing(thing) {
  // ...
}

export default defineTests(
  suite(
    'feature area',
    test('does one thing', {
      async setup() {
        return await createThing();
      },
      async run(thing) {
        // ...
      },
      async teardown(thing) {
        await disposeThing(thing);
      },
    }),
  ),
);

That shape is easy for agents to generate, easy for humans to review, and parallel-safe by default.