About testing-flow
A skill for writing and running behavioral tests for Robomotion flows with `@robomotion/sdk/testing` and `bun test`. It covers flow inspection, service mocking, isolated Function-node tests, and subflow testing.
Key features
- 01Run `*.test.ts` with `bun test`
- 02Inspect flows and execution paths
- 03Mock external services and track calls
- 04Test Function-node code in isolation
- 05Run subflows independently
- 06Switch between unit and integration modes
Use cases
- 01Run behavioral tests for a Robomotion flow
- 02Mock services to test node behavior
- 03Check Function-node logic without running the full flow
Install
Copy the command and run it in a terminal.
$
npx skills add https:/ / github.com/ robomotionio/ agent-skills --skill testing-flowAfter installing, type/testing-flowin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
config.jsonreference/flow-tester.mdfunction-extraction.mdintegration-mode.mdmock-registry.mdsubflow-tester.mdDetails
From the project READMETesting a Robomotion Flow
Run behavioral tests for a Robomotion flow. Uses @robomotion/sdk/testing + bun test.
For pspec schema validation, use validating-flow.
Purpose
- Execute
*.test.tsfiles withbun test - Author new tests with FlowTester, MockRegistry, SubflowTester
- Inspect structure, mock services, trace execution, exercise Function-node logic
Workflow
- Check for tests —
ls path/to/flow/*.test.ts - Run —
cd path/to/flow && bun test - Fix failures — diagnose, edit, re-run
Quick Start
Create main.test.ts next to main.ts:
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { FlowTester, MockRegistry } from '@robomotion/sdk/testing';
describe('My Flow', () => {
let tester: FlowTester;
beforeAll(async () => {
tester = await FlowTester.load('./main.ts');
});
afterAll(() => {
MockRegistry.clear();
});
test('validates', () => {
expect(tester.validate().valid).toBe(true);
});
test('has expected node count', () => {
expect(tester.getNodes().length).toBe(10);
});
});
API Map
Load only the reference file(s) you need. Each is self-contained and lives inside this skill under ./reference/.
| Area | What's in it | Reference |
|---|---|---|
| Load/inspect flows, run checkpoints, analyze paths and complexity | FlowTester, analyzeFlow, findPaths, generateDotGraph | ./reference/flow-tester.md |
| Mock external services (per-node, per-type, helpers, call tracking) | MockRegistry, mockResponse/Error/Sequence/Conditional | ./reference/mock-registry.md |
| Test Function-node JavaScript in isolation | extractFunc, runFunc, testFunctionWithCases, analyzeFunctionNode | ./reference/function-extraction.md |
| Run subflows in isolation; discover them in a flow | SubflowTester, discoverSubflows, loadAllSubflows | ./reference/subflow-tester.md |
| Unit vs real-service mode via env vars | isIntegrationMode, hasCredentials, createTestHelper | ./reference/integration-mode.md |
Complete Example
import { describe, test, expect, beforeAll, afterAll, afterEach } from 'bun:test';
import {
FlowTester, MockRegistry, extractFunc,
isIntegrationMode, hasCredentials, analyzeFlow,
} from '@robomotion/sdk/testing';
describe('Reddit to WordPress Flow', () => {
let tester: FlowTester;
beforeAll(async () => { tester = await FlowTester.load('./main.ts'); });
afterEach(() => MockRegistry.resetCalls());
afterAll(() => MockRegistry.clear());
describe('Structure', () => {
test('validates', () => expect(tester.validate().valid).toBe(true));
test('all branches terminate', () => expect(tester.allBranchesTerminate()).toBe(true));
test('loops have Goto->Label', () => {
expect(tester.findLoops().every(l => l.hasGotoLabel)).toBe(true);
});
test('no dead ends', () => {
expect(analyzeFlow(tester).deadEnds).toHaveLength(0);
});
});
describe('Transform Logic', () => {
test('formats Reddit post', () => {
const transform = extractFunc(tester, '2bf967');
const out = transform({ post: { title: 'Test', selftext: 'Body', subreddit: 'programming' } });
expect(out.wpTitle).toContain('Test');
});
});
describe('With Mocked Services', () => {
beforeAll(() => {
MockRegistry.registerNode('c8f412', async () => ({
posts: [{ title: 'Mock 1', selftext: 'c1', score: 100 }],
}));
MockRegistry.register('Core.Net.HttpRequest', async (p, m, nodeId) => {
console.warn(`Unmocked HTTP in ${nodeId}`);
return { statusCode: 200, body: {} };
});
});
test('fetches posts', async () => {
const res = await tester.runUntil('c5d901', {
input: { subreddit: 'programming', limit: 10 },
});
expect(res.msg.posts).toHaveLength(1);
expect(MockRegistry.getCallCount('c8f412')).toBe(1);
});
});
describe('Integration', () => {
test('real WordPress posting', async () => {
if (!isIntegrationMode() || !hasCredentials('wordpress')) return;
const res = await tester.runWithTrace({
input: { subreddit: 'programming', postLimit: 1 },
useRealServices: true,
});
expect(res.output.publishedUrl).toBeDefined();
});
});
});
Tips
- Use node-ID mocks for precise control; type mocks as catch-all.
- Verify mock inputs with
getLastCall(). - Clear mocks in
afterAll()to avoid pollution across files. INTEGRATION=1 bun testswitches to real services.
Related Skills
validating-flow— schema validation (property names, ports, node types)creating-flow— generate TypeScriptrunning-flow— execute on a robot
Comments
Markdown supported