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
71 changes: 71 additions & 0 deletions .github/workflows/backend-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: backend-tests

# The backend pytest suite, run on every pull request and on pushes to the mainline branches. Until
# now nothing ran it in CI, so a regression only surfaced when someone ran it by hand.
on:
pull_request:
paths:
- 'backend/**'
- '.github/workflows/backend-tests.yml'
push:
branches: [main, dev]
paths:
- 'backend/**'
- '.github/workflows/backend-tests.yml'
workflow_dispatch:

# Runs checked-out project code on pull_request: the token stays read-only.
permissions:
contents: read

concurrency:
group: backend-tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
pytest:
name: pytest (ubuntu)
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13'
cache: pip
cache-dependency-path: |
backend/requirements.lock
backend/requirements-dev.txt
- name: Install backend deps (locked runtime + dev)
run: |
python -m pip install --require-hashes --only-binary=:all: -r backend/requirements.lock
python -m pip install -r backend/requirements-dev.txt
# Two numbers that must agree. A test process that dies mid-run can exit 0 with no summary
# (a hard-exit shutdown path did exactly that once and silently skipped ~42% of the suite), so
# the job also asserts that every collected test was actually run: junit testcase count ==
# collect-only count. Green then means green, not "green as far as it got".
- name: Count the suite
run: |
python -m pytest backend/tests --ignore=backend/tests/formal --collect-only -q -p no:cacheprovider \
| tail -1 | tee collected.txt
# --timeout: a test that blocks forever (a bare ws.receive_json() waiting for an event that never
# comes, say) otherwise stalls the run at 99% until timeout-minutes with no summary and no junit.
# With the cap it fails by name, the rest of the suite runs, and the assertion below still holds.
- name: Run the backend suite
run: |
python -m pytest backend/tests --ignore=backend/tests/formal -q -p no:cacheprovider \
--timeout=300 \
--junitxml "${RUNNER_TEMP}/pytest.xml"
- name: Every collected test ran
# Runs after a red suite too, so a failure report also says whether the run was complete.
if: ${{ !cancelled() }}
run: |
python - "${RUNNER_TEMP}/pytest.xml" collected.txt <<'PY'
import re, sys, xml.etree.ElementTree as ET
ran = sum(1 for _ in ET.parse(sys.argv[1]).getroot().iter('testcase'))
m = re.search(r'(\d+) tests? collected', open(sys.argv[2]).read())
collected = int(m.group(1)) if m else -1
print(f'collected={collected} ran={ran}')
if collected < 1 or ran != collected:
sys.exit(f'FAIL: {ran} of {collected} collected tests reached the report; the run was truncated')
PY
44 changes: 44 additions & 0 deletions .github/workflows/edge-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: edge-tests

# The openswarm-edge pytest suite, on every pull request that touches it and on pushes to the
# mainline branches.
on:
pull_request:
paths:
- 'openswarm-edge/**'
- '.github/workflows/edge-tests.yml'
push:
branches: [main, dev]
paths:
- 'openswarm-edge/**'
- '.github/workflows/edge-tests.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: edge-tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
pytest:
name: pytest (openswarm-edge)
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
working-directory: openswarm-edge
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13'
cache: pip
cache-dependency-path: openswarm-edge/requirements.txt
- name: Install edge deps
run: |
python -m pip install -r requirements.txt
python -m pip install pytest pytest-asyncio
- name: Run the edge suite
run: python -m pytest tests -q -p no:cacheprovider
43 changes: 43 additions & 0 deletions .github/workflows/frontend-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: frontend-tests

# Typecheck plus the renderer's node:test suite, on every pull request and on pushes to the mainline
# branches. Until now neither ran in CI; the tests were run by hand, one file at a time.
on:
pull_request:
paths:
- 'frontend/**'
- '.github/workflows/frontend-tests.yml'
push:
branches: [main, dev]
paths:
- 'frontend/**'
- '.github/workflows/frontend-tests.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: frontend-tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
typecheck-and-tests:
name: tsc + node:test
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20.18.1'
cache: npm
cache-dependency-path: frontend/package-lock.json
- run: npm ci
- name: Typecheck
run: npx tsc --noEmit -p tsconfig.json
- name: Unit tests (node:test via tsx)
run: node scripts/run-tests.mjs
105 changes: 105 additions & 0 deletions backend/apps/dashboards/dashboard_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Injected cross-app runtime boundary for the dashboards app."""

from __future__ import annotations

from typing import Any, Optional, Protocol, runtime_checkable

from backend.apps.agents import agent_manager as agents_runtime
from backend.apps.agents.core import aux_llm
from backend.apps.agents.manager.session import session_store
from backend.apps.agents.providers import registry
from backend.apps.service.analytics import client as analytics
from backend.apps.settings import credentials
from backend.apps.settings import settings as settings_app


@runtime_checkable
class DashboardTelemetry(Protocol):
"""Fire-and-forget analytics the dashboard routes emit."""

def dashboard_event(self, *, dashboard_id: str, action: str) -> None: ...


@runtime_checkable
class SessionAuthority(Protocol):
"""Live/persisted agent-session operations the dashboard routes need."""

def live_sessions(self) -> dict[str, Any]: ...

def load_session_data(self, session_id: str) -> Optional[dict]: ...

def save_session(self, session_id: str, data: dict) -> None: ...

async def delete_session(self, session_id: str) -> None: ...

async def duplicate_session(self, session_id: str, *, dashboard_id: str) -> Any: ...

def purge_session_memory(self, session_id: str) -> None: ...


@runtime_checkable
class AuxNaming(Protocol):
"""Primitive lookups behind auto-naming; prompt/stream logic stays in dashboards.py."""

def load_settings(self) -> Any: ...

async def resolve_aux_model(self, settings: Any, *, preferred_tier: str) -> tuple[str, Any]: ...

def client_for_model(self, settings: Any, model: str) -> Any: ...

def clean_short_label(self, text: str) -> str: ...

def aux_max_tokens_for(self, model: str) -> int: ...


class DefaultDashboardTelemetry:
"""Production adapter; dynamic lookups preserve established test seams."""

def dashboard_event(self, *, dashboard_id: str, action: str) -> None:
analytics.track_dashboard_event(dashboard_id=dashboard_id, action=action)


class DefaultSessionAuthority:
"""Production adapter; dynamic lookups preserve established test seams."""

def live_sessions(self) -> dict[str, Any]:
return agents_runtime.agent_manager.sessions

def load_session_data(self, session_id: str) -> Optional[dict]:
return session_store.load_session_data(session_id)

def save_session(self, session_id: str, data: dict) -> None:
session_store.save_session(session_id, data)

async def delete_session(self, session_id: str) -> None:
await agents_runtime.agent_manager.delete_session(session_id)

async def duplicate_session(self, session_id: str, *, dashboard_id: str) -> Any:
return await agents_runtime.agent_manager.duplicate_session(session_id, dashboard_id=dashboard_id)

def purge_session_memory(self, session_id: str) -> None:
agents_runtime.agent_manager.purge_session_memory(session_id)


class DefaultAuxNaming:
"""Production adapter; dynamic lookups preserve established test seams."""

def load_settings(self) -> Any:
return settings_app.load_settings()

async def resolve_aux_model(self, settings: Any, *, preferred_tier: str) -> tuple[str, Any]:
return await registry.resolve_aux_model(settings, preferred_tier=preferred_tier)

def client_for_model(self, settings: Any, model: str) -> Any:
return credentials.get_anthropic_client_for_model(settings, model)

def clean_short_label(self, text: str) -> str:
return aux_llm.clean_short_label(text)

def aux_max_tokens_for(self, model: str) -> int:
return aux_llm.aux_max_tokens_for(model)


DEFAULT_DASHBOARD_TELEMETRY: DashboardTelemetry = DefaultDashboardTelemetry()
DEFAULT_SESSION_AUTHORITY: SessionAuthority = DefaultSessionAuthority()
DEFAULT_AUX_NAMING: AuxNaming = DefaultAuxNaming()
Loading