test: add gated Azure Functions host E2E suite (Storage backend)#303
test: add gated Azure Functions host E2E suite (Storage backend)#303YunchuWang wants to merge 2 commits into
Conversation
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 |
There was a problem hiding this comment.
why renamed internal?
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
is this script used anywhere or it is intended for manual execution
There was a problem hiding this comment.
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>) => { |
There was a problem hiding this comment.
why name it counter1, it seems few functions in this pr, check if thats all from azure durable function js repo
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
where is this, you are referring to the unmerged pr v4 df js pkg path?
There was a problem hiding this comment.
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"; | |||
There was a problem hiding this comment.
we are not adding another test df app using v4 syntax?
There was a problem hiding this comment.
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'; |
What
Adds a gated Azure Functions host end-to-end (E2E) test suite plus a new CI workflow. The suite launches a real
func starthost 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-functionspackage added by #282 (packages/azure-functions-durable), which is not onmainyet. To keep this reviewable independently of #282 and to keep existing CI green onmainwhile that package is absent, everything here is gated / skipped:jest.functions-e2e.config.js+npm run test:e2e:functions:internal. They are not part ofnpm test/ workspaces,test:unit, ortest:e2e:internal(which is scoped totests/e2e). Verified withjest --listTests.globalSetuppreflight skips the whole suite unlessfuncis onPATH, Azurite is reachable on127.0.0.1:10000, and the test-app is installed + built. Onmain, the test-app can't install (missing localdurable-functions), so the suite is a clean no-op..github/workflows/functions-e2e-tests.yamlgates every job behindif: ${{ vars.RUN_FUNCTIONS_E2E == 'true' }}. It's a no-op until that repo variable is set totrue(do so after Add Functions gRPC core helpers #282 merges). Triggers:workflow_dispatch+pull_requestontest/e2e-functions/**.What it covers
Faithful to the (identical)
azure-functions-durable-jssample app:hello.spec.ts— startshelloOrchestrator(chains threehelloactivities) and asserts the output is["Hello, Tokyo", "Hello, Seattle", "Hello, Cairo"].counter1.spec.ts— signals thecounter1durable entity (add, 1) over HTTP and asserts the persisted state increments (1, then 2).resetis 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 toazure-functions-durable-js(test/test-app). The only changes are dependency wiring —durable-functionspoints at the workspace package (file:../../../packages/azure-functions-durable) — and a plain/api/pingreadiness 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
constinswitchcases,test/e2e-functions/test-appis excluded from the repo's root eslint + prettier so it stays verbatim whilenpm run lint/npm run prettyremain green. This is the only change outside the strictly-additive surface.Files
test/e2e-functions/—harness.ts(Node-onlyFunctionApplifecycle + HTTP/orchestration/entity helpers; cross-platform teardown),global-setup.ts,hello.spec.ts,counter1.spec.ts,README.md, andtest-app/(the ported sample app +ping.ts).jest.functions-e2e.config.js— dedicated, not run by default.package.json— addstest:e2e:functions:internalandtest:e2e:functionsscripts (not wired intonpm 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 buildsucceed onmainwith these changes.npm run lintclean; authored filesprettier-clean;tsc --stricttype-checks the harness + specs.npm run test:e2e:functions:internalwithout prerequisites → 2 skipped, 0 failures.npm test/test:unit/test:e2e:internaldo not pick up the new specs.How to enable (after #282 merges)
packages/azure-functions-durableexists.RUN_FUNCTIONS_E2E=true.npm ci && npm run build→ install + buildtest/e2e-functions/test-app→npm run test:e2e:functions:internal. Seetest/e2e-functions/README.md.Related: #282 · mirrors durabletask-python#155.
Co-authored-by: Copilot App [email protected]