Skip to content

Implement in-memory orchestration backend for testing#94

Merged
YunchuWang merged 7 commits intomainfrom
wangbill/testing
Feb 5, 2026
Merged

Implement in-memory orchestration backend for testing#94
YunchuWang merged 7 commits intomainfrom
wangbill/testing

Conversation

@YunchuWang
Copy link
Member

@YunchuWang YunchuWang commented Feb 5, 2026

Summary

What changed?

  • Added InMemoryOrchestrationBackend - an in-memory storage backend for orchestration state suitable for testing
  • Added TestOrchestrationClient - a client API for scheduling and managing orchestrations against the in-memory backend
  • Added TestOrchestrationWorker - a worker that processes orchestrations and activities using the real OrchestrationExecutor and ActivityExecutor
  • Exported all testing utilities from the main package via @microsoft/durabletask-js
  • Updated e2e tests to use the in-memory testing backend instead of requiring a gRPC sidecar
  • Added comprehensive unit tests for the in-memory backend

Why is this change needed?

  • Enables unit testing and integration testing of durable orchestrations without requiring a sidecar process
  • Simplifies test setup and improves test execution speed
  • Allows testing orchestration logic in isolation while still exercising the real executor code paths
  • Makes it easier for developers to write tests for their orchestration code

Issues / work items

  • Resolves #
  • Related #

Project checklist

  • Release notes are not required for the next release
    • Otherwise: Notes added to CHANGELOG.md
  • Backport is not required
    • Otherwise: Backport tracked by issue/PR #issue_or_pr
  • All required tests have been added/updated (unit tests, E2E tests)
  • Breaking change?
    • If yes:
      • Impact: N/A - This is a new feature addition
      • Migration guidance: N/A

AI-assisted code disclosure (required)

Was an AI tool used? (select one)

  • No
  • Yes, AI helped write parts of this PR (e.g., GitHub Copilot)
  • [] Yes, an AI agent generated most of this PR

If AI was used:

  • Tool(s): GitHub Copilot (Claude)
  • AI-assisted areas/files:
    • src/testing/in-memory-backend.ts - Core in-memory backend implementation
    • src/testing/test-client.ts - Test client implementation
    • src/testing/test-worker.ts - Test worker implementation
    • tests/e2e/orchestration.spec.ts - Updated to use in-memory backend
    • packages/durabletask-js/test/in-memory-backend.spec.ts - Unit tests
  • What you changed after AI output: Reviewed implementation, fixed continue-as-new event ordering, verified alignment with DTMB backend behavior

AI verification (required if AI was used):

  • I understand the code and can explain it
  • I verified referenced APIs/types exist and are correct
  • I reviewed edge cases/failure paths (timeouts, retries, cancellation, exceptions)
  • I reviewed concurrency/async behavior
  • I checked for unintended breaking or behavior changes

Testing

Automated tests

  • Result: Passed (13 tests pass, 1 skipped - PurgeInstanceCriteria not supported in in-memory backend)

Manual validation (only if runtime/behavior changed)

  • Environment (OS, Node.js version, components): Windows, Node.js
  • Steps + observed results:
    1. Ran npm run test:e2e:internal - all tests pass
    2. Verified orchestration lifecycle (create, run, complete) works correctly
    3. Verified activities, timers, sub-orchestrations, external events, termination, continue-as-new all work
  • Evidence (optional): N/A

Notes for reviewers

  • The in-memory backend uses polling (setImmediate) for sub-orchestration completion notification, which differs from the production backend's event-driven approach, but achieves the same functional behavior
  • PurgeInstanceCriteria (multi-instance purge) is not implemented - test remains skipped
  • The testing implementation reuses the real OrchestrationExecutor, ActivityExecutor, and Registry classes, ensuring that the core orchestration logic is tested

Copilot AI review requested due to automatic review settings February 5, 2026 00:59
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

This PR implements an in-memory orchestration backend for testing, eliminating the need for a gRPC sidecar process during tests. The change improves test isolation, reduces test infrastructure complexity, and makes tests faster and more reliable.

Changes:

  • Adds in-memory orchestration backend (InMemoryOrchestrationBackend) that manages orchestration state, work queues, and execution lifecycle entirely in memory
  • Implements TestOrchestrationClient and TestOrchestrationWorker classes that provide similar APIs to the gRPC-based implementations but operate against the in-memory backend
  • Updates e2e tests to use the new in-memory backend instead of connecting to a gRPC sidecar
  • Exports the new testing utilities from the package's public API

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/durabletask-js/src/testing/in-memory-backend.ts Core in-memory backend implementation managing orchestration instances, work queues, and state transitions
packages/durabletask-js/src/testing/test-client.ts Client for scheduling and managing orchestrations in the in-memory backend
packages/durabletask-js/src/testing/test-worker.ts Worker that processes orchestrations and activities from the in-memory backend
packages/durabletask-js/src/testing/index.ts Exports the testing utilities for public consumption
packages/durabletask-js/src/index.ts Adds testing utilities to the package's public API exports
packages/durabletask-js/test/in-memory-backend.spec.ts Comprehensive unit tests for the in-memory backend functionality
tests/e2e/orchestration.spec.ts Updates e2e tests to use in-memory backend; simplifies status enum usage; adds error handling in cleanup
Comments suppressed due to low confidence (1)

tests/e2e/orchestration.spec.ts:511

  • This test is skipped but contains casts to 'any' type (lines 455, 465, 502) to access purgeOrchestration with PurgeInstanceCriteria. Since TestOrchestrationClient.purgeOrchestration only accepts a string parameter (line 124 in test-client.ts), but the test attempts to use the criteria-based API, this test will fail when unskipped. Consider either implementing criteria-based purge support in the in-memory backend or documenting that this feature requires the gRPC client.
  it.skip("should be able to purge orchestration by PurgeInstanceCriteria", async () => {
    const delaySeconds = 1;
    const plusOne = async (_: ActivityContext, input: number) => {
      return input + 1;
    };

    const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, startVal: number): any {
      return yield ctx.callActivity(plusOne, startVal);
    };

    const terminate: TOrchestrator = async function* (ctx: OrchestrationContext): any {
      yield ctx.createTimer(delaySeconds);
    };

    taskHubWorker.addOrchestrator(orchestrator);
    taskHubWorker.addOrchestrator(terminate);
    taskHubWorker.addActivity(plusOne);
    await taskHubWorker.start();

    // Set startTime slightly in the past to account for clock drift
    const startTime = new Date(Date.now() - 1000);
    const id = await taskHubClient.scheduleNewOrchestration(orchestrator, 1);
    const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30);

    expect(state);
    expect(state?.name).toEqual(getName(orchestrator));
    expect(state?.instanceId).toEqual(id);
    expect(state?.failureDetails).toBeUndefined();
    expect(state?.runtimeStatus).toEqual(OrchestrationStatus.COMPLETED);
    expect(state?.serializedInput).toEqual(JSON.stringify(1));
    expect(state?.serializedOutput).toEqual(JSON.stringify(2));

    // purge instance, test CreatedTimeFrom
    const criteria = new PurgeInstanceCriteria();
    criteria.setCreatedTimeFrom(startTime);
    // Note: This uses gRPC client API, not available in in-memory backend
    let purgeResult = await (taskHubClient as any).purgeOrchestration(criteria);
    expect(purgeResult);
    expect(purgeResult?.deletedInstanceCount).toEqual(1);

    // assert instance doesn't exit.
    let metadata = await taskHubClient.getOrchestrationState(id);
    expect(metadata).toBeUndefined();

    // purge instance, test CreatedTimeTo
    criteria.setCreatedTimeTo(new Date(Date.now()));
    purgeResult = await (taskHubClient as any).purgeOrchestration(criteria);
    expect(purgeResult);
    expect(purgeResult?.deletedInstanceCount).toEqual(0);

    // assert instance doesn't exit.
    metadata = await taskHubClient.getOrchestrationState(id);
    expect(metadata).toBeUndefined();

    const id1 = await taskHubClient.scheduleNewOrchestration(orchestrator, 1);
    const state1 = await taskHubClient.waitForOrchestrationCompletion(id1, undefined, 30);

    expect(state1);
    expect(state1?.name).toEqual(getName(orchestrator));
    expect(state1?.instanceId).toEqual(id1);
    expect(state1?.failureDetails).toBeUndefined();
    expect(state1?.runtimeStatus).toEqual(OrchestrationStatus.COMPLETED);
    expect(state1?.serializedInput).toEqual(JSON.stringify(1));
    expect(state1?.serializedOutput).toEqual(JSON.stringify(2));

    const id2 = await taskHubClient.scheduleNewOrchestration(terminate);
    await taskHubClient.terminateOrchestration(id2, "termination");
    const state2 = await taskHubClient.waitForOrchestrationCompletion(id2, undefined, 30);
    expect(state2);
    expect(state2?.name).toEqual(getName(terminate));
    expect(state2?.instanceId).toEqual(id2);
    expect(state2?.failureDetails).toBeUndefined();
    expect(state2?.runtimeStatus).toEqual(OrchestrationStatus.TERMINATED);

    const runtimeStatuses: OrchestrationStatus[] = [];
    runtimeStatuses.push(OrchestrationStatus.TERMINATED);
    runtimeStatuses.push(OrchestrationStatus.COMPLETED);

    // Add a small delay to ensure the orchestrations are fully persisted
    await new Promise((resolve) => setTimeout(resolve, 1000));

    criteria.setCreatedTimeTo(new Date(Date.now()));
    criteria.setRuntimeStatusList(runtimeStatuses);
    purgeResult = await (taskHubClient as any).purgeOrchestration(criteria);
    expect(purgeResult);
    expect(purgeResult?.deletedInstanceCount).toEqual(2);

    // assert instance doesn't exit.
    metadata = await taskHubClient.getOrchestrationState(id1);
    expect(metadata).toBeUndefined();
    metadata = await taskHubClient.getOrchestrationState(id2);
    expect(metadata).toBeUndefined();
  }, 31000);

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

@YunchuWang YunchuWang merged commit af18105 into main Feb 5, 2026
7 checks passed
@YunchuWang YunchuWang deleted the wangbill/testing branch February 5, 2026 17:18
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.

2 participants