Implement in-memory orchestration backend for testing#94
Merged
YunchuWang merged 7 commits intomainfrom Feb 5, 2026
Merged
Conversation
There was a problem hiding this comment.
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
TestOrchestrationClientandTestOrchestrationWorkerclasses 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.
…r improved resource management
…letask-js into wangbill/testing
kaibocai
approved these changes
Feb 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
What changed?
InMemoryOrchestrationBackend- an in-memory storage backend for orchestration state suitable for testingTestOrchestrationClient- a client API for scheduling and managing orchestrations against the in-memory backendTestOrchestrationWorker- a worker that processes orchestrations and activities using the realOrchestrationExecutorandActivityExecutor@microsoft/durabletask-jsWhy is this change needed?
Issues / work items
Project checklist
CHANGELOG.mdAI-assisted code disclosure (required)
Was an AI tool used? (select one)
If AI was used:
src/testing/in-memory-backend.ts- Core in-memory backend implementationsrc/testing/test-client.ts- Test client implementationsrc/testing/test-worker.ts- Test worker implementationtests/e2e/orchestration.spec.ts- Updated to use in-memory backendpackages/durabletask-js/test/in-memory-backend.spec.ts- Unit testsAI verification (required if AI was used):
Testing
Automated tests
Manual validation (only if runtime/behavior changed)
npm run test:e2e:internal- all tests passNotes for reviewers
setImmediate) for sub-orchestration completion notification, which differs from the production backend's event-driven approach, but achieves the same functional behaviorPurgeInstanceCriteria(multi-instance purge) is not implemented - test remains skippedOrchestrationExecutor,ActivityExecutor, andRegistryclasses, ensuring that the core orchestration logic is tested