Skip to content
Merged
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
37 changes: 19 additions & 18 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
black
isort
mypy
flake8
flake8-toml-config
pytest
black==26.3.1
flake8==7.3.0
flake8-toml-config==1.0.0
isort==8.0.1
mypy==1.20.2
pytest==9.0.3
pytest-asyncio==1.4.0


# server
numpy
ray[serve]
webrtcvad
faster-whisper
qdrant-client
sentence-transformers
pypdf
semantic_chunker
numpy==1.26.4
ray[serve]==2.55.1
webrtcvad==2.0.10
faster-whisper==1.2.1
qdrant-client==1.18.0
sentence-transformers==3.2.1
pypdf==5.1.0
semantic_chunker==0.2.0


# client
sounddevice
websockets
omegaconf
prompt-toolkit
sounddevice==0.5.5
websockets==16.0
omegaconf==2.3.0
prompt-toolkit==3.0.52
Empty file added tests/__init__.py
Empty file.
Empty file added tests/core/__init__.py
Empty file.
230 changes: 230 additions & 0 deletions tests/core/test_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import asyncio
import logging
from dataclasses import dataclass

import numpy as np
import pytest

from src.core.events import EventData, EventGraph, _summarize

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


class DummyModule:
input_type = None
output_type = None

async def process(self, data):
raise NotImplementedError


@dataclass
class DummyEvent(EventData):
value: int


@dataclass
class ArrayEvent(EventData):
data: np.ndarray


# ---------------------------------------------------------------------------
# register()
# ---------------------------------------------------------------------------


def test_register():
graph = EventGraph()

module = DummyModule()
module.input_type = "input"

graph.register(module)

assert module in graph.subscribers["input"]


# ---------------------------------------------------------------------------
# publish()
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_publish_calls_subscriber(monkeypatch):
graph = EventGraph()

called = asyncio.Event()

class Module(DummyModule):
input_type = "input"

async def process(self, data):
called.set()
return None

module = Module()
graph.register(module)

await graph.publish("input", DummyEvent(1))

await asyncio.wait_for(called.wait(), timeout=1)


# ---------------------------------------------------------------------------
# coroutine output
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_coroutine_output_published():
graph = EventGraph()

received = asyncio.Event()

class Producer(DummyModule):
input_type = "start"
output_type = "next"

async def process(self, data):
return DummyEvent(42)

class Consumer(DummyModule):
input_type = "next"

async def process(self, data):
assert data.value == 42
received.set()

graph.register(Producer())
graph.register(Consumer())

await graph.publish("start", DummyEvent(0))

await asyncio.wait_for(received.wait(), timeout=1)


# ---------------------------------------------------------------------------
# async generator output
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_async_generator_output():
graph = EventGraph()

values = []

class Producer(DummyModule):
input_type = "start"
output_type = "stream"

async def process(self, data):
yield DummyEvent(1)
yield DummyEvent(2)
yield DummyEvent(3)

class Consumer(DummyModule):
input_type = "stream"

async def process(self, data):
values.append(data.value)

graph.register(Producer())
graph.register(Consumer())

await graph.publish("start", DummyEvent(0))

await asyncio.sleep(0.1)

assert values == [1, 2, 3]


# ---------------------------------------------------------------------------
# None outputs ignored
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_none_output_not_published():
graph = EventGraph()

called = False

class Producer(DummyModule):
input_type = "start"
output_type = "next"

async def process(self, data):
return None

class Consumer(DummyModule):
input_type = "next"

async def process(self, data):
nonlocal called
called = True

graph.register(Producer())
graph.register(Consumer())

await graph.publish("start", DummyEvent(0))
await asyncio.sleep(0.05)

assert not called


# ---------------------------------------------------------------------------
# recursive routing
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_recursive_routing():
graph = EventGraph()

done = asyncio.Event()

class First(DummyModule):
input_type = "a"
output_type = "b"

async def process(self, data):
return DummyEvent(data.value + 1)

class Second(DummyModule):
input_type = "b"

async def process(self, data):
assert data.value == 2
done.set()

graph.register(First())
graph.register(Second())

await graph.publish("a", DummyEvent(1))

await asyncio.wait_for(done.wait(), timeout=1)


# ---------------------------------------------------------------------------
# summarize()
# ---------------------------------------------------------------------------


def test_summarize_numpy():
event = ArrayEvent(np.zeros((3, 4), dtype=np.float32))

text = _summarize(event)

assert "shape=(3, 4)" in text
assert "float32" in text


def test_summarize_regular():
event = DummyEvent(123)

text = _summarize(event)

assert "DummyEvent" in text