npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@identity-flow/testing

v0.3.0

Published

Public Workflow Test contracts and runner-neutral integration helpers.

Readme

@identity-flow/testing

Public Workflow Test contracts and runner-neutral integration helpers.

Introduction

The public @identity-flow/testing package provides two surfaces:

  • the types and editor contract for workflowTest() and its Vitest setup; and
  • createHookTestHost() — a runner-neutral host for IdentityFlow hook definitions that needs no database, container, or network.

The package deliberately contains no Workflow Engine, Query implementation, PostgreSQL lifecycle, or private Workflow Test runtime. Calling workflowTest() or defineWorkflowTestSetup() from the public package fails closed. The customer-authorized IdentityFlow Test distribution replaces those entrypoints when it hosts Vitest. Authorized customers obtain the exact-version distribution from the IdentityFlow download portal; it is never part of npm or the Workshop Starter.

Version cohort

Gate G3 — package publication. @identity-flow/[email protected] belongs to the exact IdentityFlow 0.3.0 public package cohort. Use the same exact version for every @identity-flow/* package in one project. The package contains only its public contract and helpers and exposes no private runtime dependency. @identity-flow/sdk/testing is not a valid entry point.

Workflow Test contract

Install

A Workflow Author works in a normal TypeScript project outside this repository. The supported runtime is Node.js 26.8.1 or newer within Node 26, with Vitest 4.1.9 or a compatible 4.x release.

pnpm add @identity-flow/[email protected] @identity-flow/[email protected] @identity-flow/[email protected]
pnpm add --save-dev @identity-flow/[email protected] @types/[email protected] [email protected] [email protected]

vitest is a required peer for the public TypeScript contract. Runtime and PostgreSQL dependencies arrive only in the exact-version Test distribution from the authorized IdentityFlow download portal.

Repository maintainers can run pnpm --filter @identity-flow/testing run test:external-consumer to build and verify an isolated consumer project.

Setup contract

This file is type-checked against the public package. It becomes executable only when the matching Test distribution from the authorized download portal hosts Vitest.

import { defineWorkflowTestSetup } from '@identity-flow/testing/vitest';

export default defineWorkflowTestSetup();

Register the file as a Vitest global setup:

import { defineConfig } from 'vitest/config';

// Workflow Tests use the `*.case.ts` suffix, which Vitest's default `include`
// does not match. Declaring it here is the only supported way to select them.
export default defineConfig({
	test: { include: ['**/*.case.ts'], globalSetup: ['./workflow-test.setup.ts'] },
});

The runtime contract selects a local or external server explicitly with defineWorkflowTestSetup({ postgres: { mode: 'supplied', connection: … } }). Supplied Mode never falls back to Managed Mode, requires allowRemote: true for every non-loopback host, and ignores generic PG* environment variables.

Bindings and the Workflow Definition

A fixture-capable Binding needs a stable name from defineBinding(name, factory). Lookup uses that name, never JavaScript function identity.

import { defineBinding } from '@identity-flow/sdk';
import { vi } from 'vitest';

export interface Directory {
	lookup(userId: string): Promise<{ readonly displayName: string }>;
}

// A production project writes the factory directly. This acceptance fixture wraps
// it in a Vitest spy so the tests can prove the Workflow Test never falls back to
// the production Binding.
export const productionDirectory = vi.fn((): Directory => {
	throw new Error('The production Directory Binding must not run in a Workflow Test');
});

export const Directory = defineBinding('directory', productionDirectory);
import * as v from '@identity-flow/sdk/valibot';
import { defineWorkflow } from '@identity-flow/sdk';

import { Directory } from './directory.binding';

export const approval = defineWorkflow(
	{ name: 'approval', version: '1.0.0', schema: v.object({ userId: v.string() }) },
	async (flow) => {
		const directory = flow.use(Directory);
		const person = await flow.do('load person', () => directory.lookup(flow.params.userId));
		const decision = await flow.dialog(
			'approve access',
			{ schema: v.object({ status: v.literal('approved') }) },
			() => ({
				params: { displayName: person.displayName },
				assignees: [{ providerId: 'acme', subject: 'role:approver' }],
			}),
		);

		return { person, decision };
	},
);

The test

The contract says that workflowTest(options) returns Vitest's ordinary test function with three per-test fixtures: flow, accounts, and fixture. The authorized Test distribution supplies that implementation.

import { WorkflowTestError, workflowTest } from '@identity-flow/testing';
import { expect, vi } from 'vitest';

import { approval } from './approval.workflow';
import { Directory, productionDirectory } from './directory.binding';

const test = workflowTest({
	workflow: approval,
	accounts: {
		requester: { providerId: 'acme', principals: ['user:requester'] },
		approver: { providerId: 'acme', principals: ['role:approver'] },
		blocked: { providerId: 'acme', principals: ['user:blocked'] },
	},
});

test('approves access', async ({ flow, accounts, fixture }) => {
	const lookup = vi.fn(() => Promise.resolve({ displayName: 'Alex Example' }));
	fixture(Directory, { lookup });

	const instance = await flow.actAs(accounts.requester).start({ userId: 'user-1234' });
	const dialog = await flow.waitFor(instance).toHaveDialog('approve access');

	await expect(
		flow.actAs(accounts.blocked).completeActivity(dialog, { status: 'approved' }),
	).rejects.toBeInstanceOf(WorkflowTestError);

	await flow.actAs(accounts.approver).completeActivity(dialog, { status: 'approved' });
	const completed = await flow.waitFor(instance).toBeCompleted();
	const observed = await flow.observe(instance);

	expect(completed.data).toEqual({
		person: { displayName: 'Alex Example' },
		decision: { status: 'approved' },
	});
	expect(observed.instance.id).toBe(instance.id);
	expect(observed.steps).toEqual(
		expect.arrayContaining([
			expect.objectContaining({ name: 'load person', status: 'COMPLETED' }),
			expect.objectContaining({ name: 'approve access', status: 'COMPLETED' }),
		]),
	);
	expect(observed.events.length).toBeGreaterThan(0);
	expect(lookup).toHaveBeenCalledOnce();
	expect(lookup).toHaveBeenCalledWith('user-1234');
	expect(productionDirectory).not.toHaveBeenCalled();
});
pnpm exec tsc --noEmit

This public-package smoke stops at import and type checking. Do not run the example with plain vitest: the exact-version Test distribution from the authorized IdentityFlow download portal owns the supported runtime command and its release-locked Engine cohort.

Author API

| Member | Contract | | ---------------------------------- | --------------------------------------------------------------------------- | | accounts.<name> | opaque handle for a declared Account; no ID or Actor is exposed | | fixture(binding, value) | typed, synchronous Binding Fixture for this test; sealed at the first start | | flow.actAs(account) | frozen driver bound to that Account's Actor | | .start(params) | starts the configured Workflow; resolves when the mutation has committed | | .completeActivity(step, result) | completes a pending Dialog; resolves when the mutation has committed | | flow.waitFor(instance, options?) | vi.waitFor semantics with interval and timeout | | .toHaveDialog(name) | resolves with the pending Dialog Step | | .toHaveChild(name, definition) | resolves with the child Instance and its typed Workflow Test driver | | .toBeCompleted() | resolves with the completed Instance and its inferred data | | flow.observe(instance) | one fresh read of that Instance, its Steps, and its Events |

Declared Principals cross the normal provider boundary, and Dialog assignees are enforced by the Engine. A declared Account that is not an assignee is denied. Awaiting a mutation proves the commit; later automatic Steps need waitFor().

Repeated flow.use() returns the same instrumented fixture, duplicate Binding Names fail at registration, unused fixtures are allowed, and your own vi.fn spies stay authoritative for calls, this, results, and response sequences.

observe() is an assertion surface, not a logging surface. It returns real data, so keep production secrets out of Workflow Tests.

Child workflows

Deploy child definitions with workflowTest({ workflow: parent, workflows: [child], accounts }). Use flow.waitFor(instance, options).toHaveChild(stepName, child) to obtain { instance, flow }. The child's flow has the same actAs, completeActivity, waitFor().toBeCompleted(), and observe contract, with the child's result type. Nested children use the same contract. Accounts and Binding Fixtures are shared within the isolated test Engine.

Child discovery follows the parent's relying Event to the child's recorded causation ID, including children that already completed. Discovery and completion use the existing wait budget. The Engine continues the parent after the child reaches terminal state; always wait for parent completion separately. Refusal returned as business data reaches the parent normally. An uncaught child error propagates through the Engine and produces WF_TEST_INSTANCE_ERRORED.

Diagnostics

Failures raise one public WorkflowTestError with a stable code: WF_TEST_WAIT_TIMEOUT, WF_TEST_MUTATION_REJECTED, WF_TEST_BINDING_FIXTURE_MISSING, WF_TEST_UNSUPPORTED_CALL, WF_TEST_CHILD_DEFINITION_MISMATCH, or WF_TEST_INSTANCE_ERRORED.

flow.request() remains unsupported at every depth. A wait checks descendants even when the test has not obtained their handles. It fails promptly with WF_TEST_UNSUPPORTED_CALL and names the API and Step. A SUBFLOW error before a child is created, including an undeployed definition or unavailable version, uses the same code. An unregistered or mismatched definition passed to toHaveChild fails with WF_TEST_CHILD_DEFINITION_MISMATCH. flow.sleep() uses real time, so a duration beyond the wait budget reports WF_TEST_WAIT_TIMEOUT.

A Binding without a fixture fails at first use and never calls the production factory:

WorkflowTestError: [WF_TEST_BINDING_FIXTURE_MISSING] An executed Binding has no registered Fixture.
Expected: Binding Fixture "directory" registered before start()
Actual: Binding Fixture "directory" missing at first use
Workflow: "diagnostic missing binding fixture" @ "1.0.0"
Step: "load person" (TASK, ERRORED, attempts 1)
Actor: account "requester"
Cause: (none)
Binding: "directory" (missing)
Events (4 shown, 4 total; chronological, safe metadata tie-break):
  - INSTANCE_STARTED
  - STEP_STARTED, step "load person", kind TASK, attempt 1
  - STEP_ERRORED, step "load person", kind TASK, attempt 1
  - INSTANCE_ERRORED
Binding calls: (none)
Next: Register it with fixture("directory", value) before start().
Redaction: params, dialog results, event payloads, Binding arguments/results, secrets, tokens, and raw Error details omitted.

Messages use a positive allowlist. Workflow parameters, Dialog values, Event payloads, Binding arguments and results, Account fields, Principal subjects, secrets, tokens, technical IDs, and raw errors never appear, and no raw error is attached as Error.cause.

Test-distribution PostgreSQL contract

| Mode | Server | Teardown | | ------------------- | --------------------------------------------- | ------------------------------------ | | Managed (default) | one pinned PostgreSQL 18.6 container per run | container is stopped and removed | | Supplied (explicit) | the server named in defineWorkflowTestSetup | server and shared extension are kept |

The authorized Test runtime gives every test one isolated schema with an ownership marker, full Engine migrations, a fresh Engine, and its own Accounts, Principals, and Binding Fixture registry. After every test — including failing ones — the Engine closes first, the schema prefix, UUID shape, marker row, and run UUID are verified, and only that exact schema is dropped. A failed check refuses the drop. The MVP never creates or drops a database and never sweeps orphan schemas, so a crashed Supplied Mode run can leave one marked schema behind.

Out of scope

Scenario DSLs, controlled time, sleep(), retry scripting, Engine restart or replay, pause, resume, terminate, reassignment, browser tests, mock OIDC, real Connectors, Staging, traces, and embedded PostgreSQL are not part of this MVP.

Hook-host tests

import * as v from 'valibot';
import { defineHooks } from '@identity-flow/sdk';
import { createHookTestHost } from '@identity-flow/testing';

const hooks = defineHooks('directory', (context) => {
	context.identity.provider('directory', {
		principals: () => [{ providerId: 'directory', subject: 'user:42' }],
	});
	context.assignees.resolver('directory', {
		context: v.strictObject({ requestId: v.string() }),
		resolve: ({ source }) => ({
			assignees: [{ providerId: 'directory', subject: `role:${source.context.requestId}` }],
		}),
	});
});

const host = createHookTestHost();
host.load(hooks);

const principals = await host.identity.resolve('directory', account);
const assignees = await host.assignees.resolve('directory', {
	source: { providerId: 'directory', sourceKey: 'request:42', context: { requestId: 'approver' } },
	signal: new AbortController().signal,
});

host.identity.calls and host.assignees.calls record validated calls in invocation order. createFakeIdentityProvider() provides the same runner-neutral call recording when a test only needs the provider boundary.

See the testing strategies guide for the hook-host, engine, and application test boundaries.

HTTP webhooks

Registered webhooks are available in host.http.webhooks. Call host.http.map(hookName, webhookName, input) to validate a JSON body, invoke the mapper, and validate every returned change envelope. Validated calls are recorded in host.http.calls; authentication and raw-byte HMAC behavior remain host-runtime concerns and are not simulated by this runner-neutral helper.

Lifecycle startup

Call host.lifecycle.start(signal?) to run registered startup adapters. Canonical change envelopes submitted by those adapters are recorded in host.lifecycle.submissions; host.lifecycle.stop() runs returned disposers in reverse order.

License

This project is licensed under the MIT License - see the LICENSE file for details.