Testing 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.ts files with bun 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 test switches to real services.
Related Skills
validating-flow — schema validation (property names, ports, node types)
creating-flow — generate TypeScript
running-flow — execute on a robot
1---2name: testing-flow3description: Runs and authors behavioral tests for Robomotion flows using @robomotion/sdk/testing. Use when the user says "test the flow", "write tests for main.ts", "mock this service", "check branch coverage", or when a flow needs regression tests before a change. For pspec schema validation, use validating-flow instead.4---56# Testing a Robomotion Flow78Run behavioral tests for a Robomotion flow. Uses `@robomotion/sdk/testing` + `bun test`.910**For pspec schema validation, use `validating-flow`.**1112## Purpose1314- Execute `*.test.ts` files with `bun test`15- Author new tests with FlowTester, MockRegistry, SubflowTester16- Inspect structure, mock services, trace execution, exercise Function-node logic1718## Workflow19201. **Check for tests** — `ls path/to/flow/*.test.ts`212. **Run** — `cd path/to/flow && bun test`223. **Fix failures** — diagnose, edit, re-run2324## Quick Start2526Create `main.test.ts` next to `main.ts`:2728```typescript29import { describe, test, expect, beforeAll, afterAll } from 'bun:test';30import { FlowTester, MockRegistry } from '@robomotion/sdk/testing';3132describe('My Flow', () => {33 let tester: FlowTester;3435 beforeAll(async () => {36 tester = await FlowTester.load('./main.ts');37 });3839 afterAll(() => {40 MockRegistry.clear();41 });4243 test('validates', () => {44 expect(tester.validate().valid).toBe(true);45 });4647 test('has expected node count', () => {48 expect(tester.getNodes().length).toBe(10);49 });50});51```5253## API Map5455Load only the reference file(s) you need. Each is self-contained and lives inside this skill under `./reference/`.5657| Area | What's in it | Reference |58|------|--------------|-----------|59| Load/inspect flows, run checkpoints, analyze paths and complexity | `FlowTester`, `analyzeFlow`, `findPaths`, `generateDotGraph` | `./reference/flow-tester.md` |60| Mock external services (per-node, per-type, helpers, call tracking) | `MockRegistry`, `mockResponse/Error/Sequence/Conditional` | `./reference/mock-registry.md` |61| Test Function-node JavaScript in isolation | `extractFunc`, `runFunc`, `testFunctionWithCases`, `analyzeFunctionNode` | `./reference/function-extraction.md` |62| Run subflows in isolation; discover them in a flow | `SubflowTester`, `discoverSubflows`, `loadAllSubflows` | `./reference/subflow-tester.md` |63| Unit vs real-service mode via env vars | `isIntegrationMode`, `hasCredentials`, `createTestHelper` | `./reference/integration-mode.md` |6465## Complete Example6667```typescript68import { describe, test, expect, beforeAll, afterAll, afterEach } from 'bun:test';69import {70 FlowTester, MockRegistry, extractFunc,71 isIntegrationMode, hasCredentials, analyzeFlow,72} from '@robomotion/sdk/testing';7374describe('Reddit to WordPress Flow', () => {75 let tester: FlowTester;7677 beforeAll(async () => { tester = await FlowTester.load('./main.ts'); });78 afterEach(() => MockRegistry.resetCalls());79 afterAll(() => MockRegistry.clear());8081 describe('Structure', () => {82 test('validates', () => expect(tester.validate().valid).toBe(true));83 test('all branches terminate', () => expect(tester.allBranchesTerminate()).toBe(true));84 test('loops have Goto->Label', () => {85 expect(tester.findLoops().every(l => l.hasGotoLabel)).toBe(true);86 });87 test('no dead ends', () => {88 expect(analyzeFlow(tester).deadEnds).toHaveLength(0);89 });90 });9192 describe('Transform Logic', () => {93 test('formats Reddit post', () => {94 const transform = extractFunc(tester, '2bf967');95 const out = transform({ post: { title: 'Test', selftext: 'Body', subreddit: 'programming' } });96 expect(out.wpTitle).toContain('Test');97 });98 });99100 describe('With Mocked Services', () => {101 beforeAll(() => {102 MockRegistry.registerNode('c8f412', async () => ({103 posts: [{ title: 'Mock 1', selftext: 'c1', score: 100 }],104 }));105 MockRegistry.register('Core.Net.HttpRequest', async (p, m, nodeId) => {106 console.warn(`Unmocked HTTP in ${nodeId}`);107 return { statusCode: 200, body: {} };108 });109 });110111 test('fetches posts', async () => {112 const res = await tester.runUntil('c5d901', {113 input: { subreddit: 'programming', limit: 10 },114 });115 expect(res.msg.posts).toHaveLength(1);116 expect(MockRegistry.getCallCount('c8f412')).toBe(1);117 });118 });119120 describe('Integration', () => {121 test('real WordPress posting', async () => {122 if (!isIntegrationMode() || !hasCredentials('wordpress')) return;123 const res = await tester.runWithTrace({124 input: { subreddit: 'programming', postLimit: 1 },125 useRealServices: true,126 });127 expect(res.output.publishedUrl).toBeDefined();128 });129 });130});131```132133## Tips134135- Use node-ID mocks for precise control; type mocks as catch-all.136- Verify mock inputs with `getLastCall()`.137- Clear mocks in `afterAll()` to avoid pollution across files.138- `INTEGRATION=1 bun test` switches to real services.139140## Related Skills141142- `validating-flow` — schema validation (property names, ports, node types)143- `creating-flow` — generate TypeScript144- `running-flow` — execute on a robot