Skip to content

test: add gated Azure Functions host E2E suite (Storage backend)#303

Draft
YunchuWang wants to merge 2 commits into
mainfrom
yunchuwang-yunchuwang-functions-e2e-tests
Draft

test: add gated Azure Functions host E2E suite (Storage backend)#303
YunchuWang wants to merge 2 commits into
mainfrom
yunchuwang-yunchuwang-functions-e2e-tests

Conversation

@YunchuWang

Copy link
Copy Markdown
Member

What

Adds a gated Azure Functions host end-to-end (E2E) test suite plus a new CI workflow. The suite launches a real func start host for a sample function app (backed by Azurite / AzureStorage) and drives it over HTTP, exercising the full Node worker ↔ host ↔ Durable-extension path.

This mirrors what durabletask-python #155 added for Python, adapted to TypeScript/jest.

Why it's split out / gated

It exercises the in-repo durable-functions package added by #282 (packages/azure-functions-durable), which is not on main yet. To keep this reviewable independently of #282 and to keep existing CI green on main while that package is absent, everything here is gated / skipped:

  • Isolated jest config. The specs run only via jest.functions-e2e.config.js + npm run test:e2e:functions:internal. They are not part of npm test / workspaces, test:unit, or test:e2e:internal (which is scoped to tests/e2e). Verified with jest --listTests.
  • Runtime skip guard. A jest globalSetup preflight skips the whole suite unless func is on PATH, Azurite is reachable on 127.0.0.1:10000, and the test-app is installed + built. On main, the test-app can't install (missing local durable-functions), so the suite is a clean no-op.
  • Gated CI. .github/workflows/functions-e2e-tests.yaml gates every job behind if: ${{ vars.RUN_FUNCTIONS_E2E == 'true' }}. It's a no-op until that repo variable is set to true (do so after Add Functions gRPC core helpers #282 merges). Triggers: workflow_dispatch + pull_request on test/e2e-functions/**.

What it covers

Faithful to the (identical) azure-functions-durable-js sample app:

  • hello.spec.ts — starts helloOrchestrator (chains three hello activities) and asserts the output is ["Hello, Tokyo", "Hello, Seattle", "Hello, Cairo"].
  • counter1.spec.ts — signals the counter1 durable entity (add, 1) over HTTP and asserts the persisted state increments (1, then 2). reset is not wired to an HTTP route in the sample app, so it is not tested.

Faithfulness to the source app

The Durable function code (hello.ts, counter1.ts) is kept byte-for-byte identical to azure-functions-durable-js (test/test-app). The only changes are dependency wiring — durable-functions points at the workspace package (file:../../../packages/azure-functions-durable) — and a plain /api/ping readiness function used by the harness to detect host cold-start completion.

Because the vendored app uses a different formatting toolchain (4-space) and patterns like lexical const in switch cases, test/e2e-functions/test-app is excluded from the repo's root eslint + prettier so it stays verbatim while npm run lint / npm run pretty remain green. This is the only change outside the strictly-additive surface.

Files

  • test/e2e-functions/harness.ts (Node-only FunctionApp lifecycle + HTTP/orchestration/entity helpers; cross-platform teardown), global-setup.ts, hello.spec.ts, counter1.spec.ts, README.md, and test-app/ (the ported sample app + ping.ts).
  • jest.functions-e2e.config.js — dedicated, not run by default.
  • package.json — adds test:e2e:functions:internal and test:e2e:functions scripts (not wired into npm test).
  • scripts/test-e2e-functions.sh — convenience wrapper.
  • .github/workflows/functions-e2e-tests.yaml — gated CI workflow.
  • .prettierignore, eslint.config.mjs — exclude the vendored test-app.

Verification

  • npm ci + npm run build succeed on main with these changes.
  • npm run lint clean; authored files prettier-clean; tsc --strict type-checks the harness + specs.
  • npm run test:e2e:functions:internal without prerequisites → 2 skipped, 0 failures.
  • npm test / test:unit / test:e2e:internal do not pick up the new specs.

How to enable (after #282 merges)

  1. Merge Add Functions gRPC core helpers #282 so packages/azure-functions-durable exists.
  2. Set repo variable RUN_FUNCTIONS_E2E=true.
  3. Locally: start Azurite → npm ci && npm run build → install + build test/e2e-functions/test-appnpm run test:e2e:functions:internal. See test/e2e-functions/README.md.

Related: #282 · mirrors durabletask-python#155.

Co-authored-by: Copilot App [email protected]

Add an Azure Functions host end-to-end test suite that launches a real
`func start` host for a sample app (backed by Azurite / AzureStorage) and
drives it over HTTP, mirroring durabletask-python #155. It exercises the
in-repo `durable-functions` package added by #282
(packages/azure-functions-durable), which is not on main yet.

Everything is gated/skipped so existing CI stays green on main until #282
merges:
- The suite runs only via a dedicated jest config
  (jest.functions-e2e.config.js) + `npm run test:e2e:functions:internal`;
  it is not part of `npm test` / workspaces / `test:e2e:internal`.
- Specs skip cleanly unless `func`, Azurite (127.0.0.1:10000) and a
  built/installed test-app are all present (jest globalSetup preflight).
- The new CI workflow gates every job behind the RUN_FUNCTIONS_E2E repo
  variable, so it is a no-op until set to 'true' after #282 lands.

The sample app functions are kept byte-for-byte identical to
azure-functions-durable-js; only the `durable-functions` dependency is
rewired to the workspace package, plus a plain /api/ping readiness function.
The vendored test-app is excluded from root eslint + prettier to preserve it
verbatim while keeping lint/format green.

Co-authored-by: Copilot App <[email protected]>
npm run build

- name: ✅ Run Functions host E2E tests
run: npm run test:e2e:functions:internal

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

why renamed internal?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Renamed for consistency with the existing convention in this repo (test:e2e:internal / test:e2e:azuremanaged:internal): the :internal script is the bare jest invocation, and the non-:internal wrapper (scripts/test-e2e-functions.sh) handles Azurite startup + test-app install/build before delegating to it. CI calls the :internal jest directly after doing that setup itself.

@@ -0,0 +1,66 @@
#!/bin/bash

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

is this script used anywhere or it is intended for manual execution

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes — it's the local/manual entry point and mirrors the sibling e2e scripts. It starts Azurite, installs+builds the test-app, then runs the :internal jest. CI does the equivalent steps inline in .github/workflows/functions-e2e-tests.yaml (so the workflow doesn't depend on the shell script). Updated it in this push to drop the old PR#282 workspace-build/package-existence checks.


const entityName = "counter1";

const counter1: EntityHandler<number> = (context: EntityContext<number>) => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

why name it counter1, it seems few functions in this pr, check if thats all from azure durable function js repo

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No — that was just the initial smoke app. This push replaces it with a faithful port of the extension repo's full BasicNode app + xUnit E2E suite (13 area specs: hello-cities, activity-input-type, error-handling, external-event, suspend-resume, terminate, timeout, rewind, is-replaying, large-output, orchestration-query, purge, class-based-entity). counter1.ts/hello.ts are removed.

},
"dependencies": {
"@azure/functions": "^4.14.0",
"durable-functions": "file:../../../packages/azure-functions-durable"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

where is this, you are referring to the unmerged pr v4 df js pkg path?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removed. That file:../../../packages/azure-functions-durable path pointed at a package that doesn't exist on this branch, which blocked the build and is why the suite never actually ran. The test-app now depends on the published durable-functions ^3.1.0 + @azure/functions ^4.11.2 so the suite is genuinely runnable; a comment in package.json notes how to repoint at an in-repo package later.

@@ -0,0 +1,41 @@
import { app, HttpHandler, HttpRequest, HttpResponse, InvocationContext } from "@azure/functions";

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

we are not adding another test df app using v4 syntax?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in this push — the test-app is now a full v4-programming-model durable-functions app (the ported BasicNode), not just hello. hello.ts is removed.

Replace the thin 2-function smoke app with a faithful port of the extension
repo's BasicNode app and xUnit E2E suite, wired to the published
durable-functions (^3.1.0) + @azure/functions (^4.11.2) so the suite is
genuinely runnable rather than blocked on an unmerged in-repo package.

App (test-app/): port all orchestrators, activities, class-based entities and
client-operation HTTP triggers verbatim (HelloCities, ActivityInputType,
Suspend/Resume, Terminate, Timeout, Rewind, IsReplaying checks, LargeOutput,
activity/entity error handling, orchestration query, purge), plus Shared/
ExceptionTypes.ts and a generic StartOrchestration starter.

Harness: generalize to mirror DurableHelpers/HttpHelpers (invokeHttpTrigger,
parseInstanceId/parseStatusQueryGetUri, waitForOrchestrationState,
getOrchestrationDetails) and add the Node localizer strings. Keep the shared
single-host lifecycle with func/Azurite preflight + clean skip.

Specs: 13 area specs ported faithfully from the xUnit tests using the Node
localizer strings. Node/Node-DTS skip traits translated (Storage backend keeps
Node-DTS tests); Node bug annotations honored (#642, #564, #608, #645, #679,
#644, and the suspend/resume/terminate-of-terminal swallow quirk).

Infra: remove the unmerged-package dependency and PR#282 gating language from
the workflow, script, eslint/prettier ignores; rewrite the README with the new
scenarios, prerequisites, run instructions and deferred follow-ups.

Verified locally: build + typecheck + eslint + prettier clean; full suite runs
green against a real func host + Azurite (13 suites, 35 passed, 10 skipped).

Co-authored-by: Copilot App <[email protected]>
let body = await request.json().catch(() => ({}));
let instanceId = body.toString() || request.query.get("instanceId") || request.params["instanceId"];
if (typeof instanceId === "object" && instanceId !== null) {
instanceId = instanceId;
import { OrchestrationHandler, OrchestrationContext, ActivityHandler } from "durable-functions";

// Helper for durations
function parseDuration(duration: string): number {

const HelloCitiesHttpStartScheduled: HttpHandler = async (request: HttpRequest, context: InvocationContext): Promise<HttpResponse> => {
const client = df.getClient(context);
const body: unknown = await request.text();

// Ported from the azure-functions-durable-extension e2e `BasicNode` app.

import { app, HttpHandler, HttpRequest, HttpResponse, HttpResponseInit, InvocationContext } from '@azure/functions';
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.

1 participant