Testing Artifacts
Write unit and integration tests for SDK artifacts — mock NodeContext, assert outputs, test error paths, and run suites from CI.
Every artifact should have a test file. Tests live in the tests/ directory alongside artifacts and run against a real execution sandbox with configurable mocking. Manage tests at SDK → Artifacts → [artifact] → Tests.
Test File Structure
import { describe, it, expect, beforeEach } from '@flowos/sdk/testing'
import { mockContext } from '@flowos/sdk/testing'
import { run } from '../artifacts/enrich-incident'
describe('enrich-incident', () => {
let ctx: ReturnType<typeof mockContext>
beforeEach(() => {
ctx = mockContext()
})
it('enriches a P1 incident with CI count and risk score', async () => {
// Arrange
ctx.tables.find.mockResolvedValue({
id: 'inc_01HX',
severity: 'P1',
cmdb_items: ['ci_01', 'ci_02', 'ci_03'],
custom_fields: {},
})
ctx.tables.update.mockResolvedValue({ id: 'inc_01HX' })
// Act
const result = await run(ctx, { incidentId: 'inc_01HX', enrichmentLevel: 'full' })
// Assert
expect(result.enriched).toBe(true)
expect(result.ciCount).toBe(3)
expect(result.riskScore).toBe(80) // 3 * 10 + 50 (P1 bonus)
expect(ctx.tables.update).toHaveBeenCalledWith('inc_01HX', expect.objectContaining({
custom_fields: { risk_score: 80 },
}))
})
it('returns lower risk score for P2 incidents', async () => {
ctx.tables.find.mockResolvedValue({
id: 'inc_02HX',
severity: 'P2',
cmdb_items: ['ci_01'],
custom_fields: {},
})
ctx.tables.update.mockResolvedValue({ id: 'inc_02HX' })
const result = await run(ctx, { incidentId: 'inc_02HX', enrichmentLevel: 'basic' })
expect(result.riskScore).toBe(30) // 1 * 10 + 20 (non-P1 bonus)
})
it('throws if incident not found', async () => {
ctx.tables.find.mockRejectedValue(new Error('Record not found'))
await expect(run(ctx, { incidentId: 'nonexistent', enrichmentLevel: 'basic' }))
.rejects.toThrow('Record not found')
})
})mockContext API
mockContext() returns a fully-typed mock NodeContext where every method is a jest.fn() / vitest mock. You control the return values per test.
import { mockContext } from '@flowos/sdk/testing'
const ctx = mockContext()
// Mock table methods
ctx.tables.find.mockResolvedValue({ id: 'inc_01HX', title: 'Test', status: 'open' })
ctx.tables.query.mockResolvedValue({ data: [/* ... */], total: 1, page: 1, pageSize: 50 })
ctx.tables.create.mockResolvedValue({ id: 'new_id' })
ctx.tables.update.mockResolvedValue({ id: 'updated_id' })
ctx.tables.delete.mockResolvedValue(undefined)
ctx.tables.sql.mockResolvedValue([{ count: 5 }])
// Mock HTTP
ctx.http.get.mockResolvedValue({ status: 200, body: { data: [] } })
ctx.http.post.mockResolvedValue({ status: 201, body: { id: 'ext-123' } })
// Mock secrets
ctx.secrets.get.mockResolvedValue('test-api-key')
// Mock notifications (fire-and-forget — usually just verify called)
ctx.notify.send.mockResolvedValue({ notificationId: 'notif_01' })
// Mock events
ctx.events.publish.mockResolvedValue({ eventId: 'evt_01' })
// Mock cache
ctx.cache.get.mockResolvedValue(null)
ctx.cache.set.mockResolvedValue(undefined)
// Assert on calls
expect(ctx.tables.update).toHaveBeenCalledWith('incidents', 'inc_01HX', { status: 'resolved' })
expect(ctx.notify.send).toHaveBeenCalledTimes(1)Setting Trigger Payload
const ctx = mockContext({
trigger: {
type: 'record_event',
payload: {
table: 'incidents',
action: 'created',
record: { id: 'inc_01HX', severity: 'P1', status: 'open' },
previous: null,
},
},
user: { id: 'usr_01HX', email: 'alice@acme.com', name: 'Alice' },
workspace: { id: 'ws_01HX', name: 'Acme IT', slug: 'acme-it', timezone: 'UTC' },
})Integration Tests
Integration tests run against a real sandbox environment with actual table reads/writes (isolated to the test environment). Use the integrationContext factory:
import { integrationContext } from '@flowos/sdk/testing'
// Uses test environment — real DB, real secrets
const ctx = await integrationContext({ environment: 'development' })
it('creates an incident end-to-end', async () => {
const incident = await ctx.tables.create('incidents', {
title: '[Test] Integration test incident',
severity: 'P4',
status: 'open',
tags: ['test'],
})
expect(incident.id).toBeTruthy()
// Clean up after test
await ctx.tables.delete('incidents', incident.id)
})development environment. They cannot target staging or production. Records created by tests are not automatically cleaned up — always delete them in an afterEach or afterAll block.Running Tests
From the UI: SDK → Artifacts → [artifact] → Tests tab → Run All
From CLI:
# Run all tests in the workspace
flowos test run
# Run tests for a specific artifact
flowos test run --artifact enrich-incident
# Run a specific test file
flowos test run --file tests/enrich-incident.test.ts
# Run in watch mode (re-runs on file save)
flowos test run --watch
# Run integration tests (requires dev environment token)
flowos test run --integration --env development
# CI mode — outputs JUnit XML
flowos test run --reporter junit --output test-results.xmlCoverage
flowos test coverage
# Outputs:
# enrich-incident.ts 92.3% (branches: 85.7%)
# github-push-handler.ts 78.1% (branches: 70.0%)
# daily-sla-report.ts 100.0% (branches: 100.0%)Coverage reports are also visible in the artifact detail page under the Tests tab.