Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,6 @@ agents/**/components/
.motia/
.mermaid/

playground/motia-workbench.json
playground/motia-workbench.json
packages/core/src/csharp/bin/*
packages/core/src/csharp/obj/*
35 changes: 35 additions & 0 deletions motia.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "packages", "packages", "{809F86A1-1C4C-B159-0CD4-DF9D33D876CE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "core", "core", "{81C5FA5E-A62B-98A1-4CBC-09B5BB06FF07}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8AF3AB25-EEF3-FF19-F39A-5E70C19766B1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MotiaCSharp", "packages\core\src\csharp\MotiaCSharp.csproj", "{FCC2B3D9-45C0-5267-DA98-CE5AA09FC76C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FCC2B3D9-45C0-5267-DA98-CE5AA09FC76C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCC2B3D9-45C0-5267-DA98-CE5AA09FC76C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCC2B3D9-45C0-5267-DA98-CE5AA09FC76C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCC2B3D9-45C0-5267-DA98-CE5AA09FC76C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{81C5FA5E-A62B-98A1-4CBC-09B5BB06FF07} = {809F86A1-1C4C-B159-0CD4-DF9D33D876CE}
{8AF3AB25-EEF3-FF19-F39A-5E70C19766B1} = {81C5FA5E-A62B-98A1-4CBC-09B5BB06FF07}
{FCC2B3D9-45C0-5267-DA98-CE5AA09FC76C} = {8AF3AB25-EEF3-FF19-F39A-5E70C19766B1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {065F5523-0B7B-46DE-AD93-B4C45F8247D6}
EndGlobalSection
EndGlobal
5 changes: 4 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
"python-setup": "python3 -m venv python_modules && python_modules/bin/pip install -r requirements.txt",
"move:python": "mkdir -p dist/src/python && cp src/python/*.py dist/src/python",
"move:rb": "mkdir -p dist/src/ruby && cp src/ruby/*.rb dist/src/ruby",
"build:csharp": "cd src/csharp && dotnet build",
"move:csharp": "mkdir -p dist/src/csharp && cp src/csharp/*.cs dist/src/csharp && cp src/csharp/*.csproj dist/src/csharp && cp src/csharp/bin/Debug/net8.0/*.dll dist/src/csharp/",
"move:steps": "cp src/steps/*.ts dist/src/steps",
"build": "rm -rf dist && tsc && npm run move:python && npm run move:rb && npm run move:steps",
"build": "if exist dist rmdir /s /q dist && tsc && npm run build:csharp && npm run move:python && npm run move:rb && npm run move:csharp && npm run move:steps",
"lint": "eslint --config ../../eslint.config.js",
"watch": "tsc --watch",
"test": "jest",
Expand Down Expand Up @@ -36,6 +38,7 @@
"@types/express": "^5.0.1",
"@types/jest": "^29.5.14",
"@types/lodash.get": "^4.4.9",
"@types/node": "^20.0.0",
"@types/node-cron": "^3.0.11",
"@types/supertest": "^6.0.2",
"@types/ws": "^8.18.1",
Expand Down
266 changes: 266 additions & 0 deletions packages/core/src/__tests__/csharp-step-execution.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import { randomUUID } from 'crypto'
import path from 'path'
import { callStepFile } from '../call-step-file'
import { createEventManager } from '../event-manager'
import { LockedData } from '../locked-data'
import { Logger } from '../logger'
import { Motia } from '../motia'
import { NoPrinter } from '../printer'
import { MemoryStateAdapter } from '../state/adapters/memory-state-adapter'
import { NoTracer } from '../observability/no-tracer'
import {
createCSharpEventStep,
createCSharpApiStep,
createCSharpCronStep,
createCSharpUiStep
} from './fixtures/csharp-step-fixtures'

describe('C# Step Execution', () => {
let baseDir: string
let eventManager: ReturnType<typeof createEventManager>
let state: MemoryStateAdapter
let motia: Motia
let printer: NoPrinter
let logger: Logger
let tracer: NoTracer

beforeAll(() => {
process.env._MOTIA_TEST_MODE = 'true'
})

beforeEach(() => {
baseDir = path.join(__dirname, 'fixtures', 'csharp')
eventManager = createEventManager()
state = new MemoryStateAdapter()
printer = new NoPrinter()
logger = new Logger()
tracer = new NoTracer()

motia = {
eventManager,
state,
printer,
lockedData: new LockedData(baseDir, 'memory', printer),
loggerFactory: { create: () => logger },
tracerFactory: { createTracer: () => tracer },
}
})

describe('Event Steps', () => {
it('should execute C# event step with context in first argument', async () => {
const step = createCSharpEventStep({
subscribes: ['test-topic'],
emits: ['test-emit']
})
const traceId = randomUUID()

jest.spyOn(eventManager, 'emit').mockImplementation(() => Promise.resolve())

await callStepFile({
step,
traceId,
logger,
contextInFirstArg: true,
tracer
}, motia)

expect(eventManager.emit).toHaveBeenCalled()
})

it('should execute C# event step with data and context', async () => {
const step = createCSharpEventStep({
subscribes: ['test-topic'],
emits: ['test-emit']
})
const traceId = randomUUID()
const testData = { message: 'Hello from C#' }

jest.spyOn(eventManager, 'emit').mockImplementation(() => Promise.resolve())

await callStepFile({
step,
traceId,
data: testData,
logger,
contextInFirstArg: false,
tracer
}, motia)

expect(eventManager.emit).toHaveBeenCalled()
})
})

describe('API Steps', () => {
it('should execute C# API step with proper configuration', async () => {
const step = createCSharpApiStep({
path: '/api/test',
method: 'POST',
bodySchema: { type: 'object' },
responseSchema: { type: 'object' }
})
const traceId = randomUUID()
const requestData = { body: { test: 'data' } }

jest.spyOn(eventManager, 'emit').mockImplementation(() => Promise.resolve())

await callStepFile({
step,
traceId,
data: requestData,
logger,
contextInFirstArg: false,
tracer
}, motia)

expect(eventManager.emit).toHaveBeenCalled()
})
})

describe('Cron Steps', () => {
it('should execute C# cron step with cron expression', async () => {
const step = createCSharpCronStep({
cron: '0 0 * * *', // Daily at midnight
emits: ['daily-event']
})
const traceId = randomUUID()

jest.spyOn(eventManager, 'emit').mockImplementation(() => Promise.resolve())

await callStepFile({
step,
traceId,
logger,
contextInFirstArg: true,
tracer
}, motia)

expect(eventManager.emit).toHaveBeenCalled()
})
})

describe('UI Steps', () => {
it('should execute C# UI step with proper configuration', async () => {
const step = createCSharpUiStep({
emits: ['ui-updated']
})
const traceId = randomUUID()

jest.spyOn(eventManager, 'emit').mockImplementation(() => Promise.resolve())

await callStepFile({
step,
traceId,
logger,
contextInFirstArg: true,
tracer
}, motia)

expect(eventManager.emit).toHaveBeenCalled()
})
})

describe('State Management', () => {
it('should handle state operations in C# steps', async () => {
const step = createCSharpEventStep({
subscribes: ['state-test']
})
const traceId = randomUUID()

// Set up state
await state.set(traceId, 'test-key', 'test-value')

jest.spyOn(eventManager, 'emit').mockImplementation(() => Promise.resolve())

await callStepFile({
step,
traceId,
logger,
contextInFirstArg: true,
tracer
}, motia)

// Verify state operations were handled
const value = await state.get(traceId, 'test-key')
expect(value).toBe('test-value')
})
})

describe('Stream Management', () => {
it('should handle stream operations in C# steps', async () => {
const step = createCSharpEventStep({
subscribes: ['stream-test']
})
const traceId = randomUUID()

jest.spyOn(eventManager, 'emit').mockImplementation(() => Promise.resolve())

await callStepFile({
step,
traceId,
logger,
contextInFirstArg: true,
tracer
}, motia)

expect(eventManager.emit).toHaveBeenCalled()
})
})

describe('Error Handling', () => {
it('should handle C# step execution errors gracefully', async () => {
const step = createCSharpEventStep({
subscribes: ['error-test']
})
const traceId = randomUUID()

// Mock a step that would cause an error
jest.spyOn(eventManager, 'emit').mockImplementation(() => Promise.reject(new Error('C# step error')))

await expect(callStepFile({
step,
traceId,
logger,
contextInFirstArg: true,
tracer
}, motia)).rejects.toThrow('C# step error')
})
})

describe('Middleware Support', () => {
it('should execute C# step with middleware', async () => {
const step = createCSharpEventStep({
subscribes: ['middleware-test'],
middleware: ['testMiddleware']
})
const traceId = randomUUID()

jest.spyOn(eventManager, 'emit').mockImplementation(() => Promise.resolve())

await callStepFile({
step,
traceId,
logger,
contextInFirstArg: true,
tracer
}, motia)

expect(eventManager.emit).toHaveBeenCalled()
})
})

describe('Configuration Validation', () => {
it('should validate C# step configuration', async () => {
const step = createCSharpEventStep({
subscribes: ['config-test'],
timeout: 5000,
retries: 3,
parallel: true
})
const traceId = randomUUID()

expect(step.config.timeout).toBe(5000)
expect(step.config.retries).toBe(3)
expect(step.config.parallel).toBe(true)
})
})
})
49 changes: 49 additions & 0 deletions packages/core/src/__tests__/fixtures/csharp-step-fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Step } from '../../types'

export const createCSharpEventStep = (config: Partial<Step['config']> = {}, filePath?: string): Step => ({
id: 'test-csharp-event-step',
filePath: filePath || 'test-csharp-event-step.cs',
config: {
type: 'event',
name: 'Test C# Event Step',
subscribes: ['test-topic'],
flows: ['test-flow'],
...config,
},
})

export const createCSharpApiStep = (config: Partial<Step['config']> = {}, filePath?: string): Step => ({
id: 'test-csharp-api-step',
filePath: filePath || 'test-csharp-api-step.cs',
config: {
type: 'api',
name: 'Test C# API Step',
path: '/test',
method: 'POST',
flows: ['test-flow'],
...config,
},
})

export const createCSharpCronStep = (config: Partial<Step['config']> = {}, filePath?: string): Step => ({
id: 'test-csharp-cron-step',
filePath: filePath || 'test-csharp-cron-step.cs',
config: {
type: 'cron',
name: 'Test C# Cron Step',
cron: '* * * * *',
flows: ['test-flow'],
...config,
},
})

export const createCSharpUiStep = (config: Partial<Step['config']> = {}, filePath?: string): Step => ({
id: 'test-csharp-ui-step',
filePath: filePath || 'test-csharp-ui-step.cs',
config: {
type: 'ui',
name: 'Test C# UI Step',
flows: ['test-flow'],
...config,
},
})
Loading