Skip to content
Open
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
150 changes: 150 additions & 0 deletions src/IskanderOS/openclaw/TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Testing the Iskander Cooperative API

This document explains how to test the Python FastAPI service at `src/IskanderOS/openclaw/`. This service is the **Iskander Cooperative API** — the governance layer that the upstream [OpenClaw](https://github.com/openclaw/openclaw) platform's Iskander-specific skills will call into. See [Discussion #190](https://github.com/Argocyte/Iskander/discussions/190) for the architectural relationship.

## Quick start — run existing unit tests

```bash
cd src/IskanderOS/openclaw
pip install -r requirements.txt
pip install pytest httpx # test dependencies
python -m pytest tests/ -v
```

All 15 existing tests should pass. They mock external HTTP calls — no running services needed.

## Local development — run the API with mock external services

The Cooperative API depends on external services (Mattermost, Loomio, Decision Recorder) that are complex to set up. For local development, a mock server emulates all of them.

### Step 1: Start the mock services

```bash
cd src/IskanderOS/openclaw
pip install uvicorn
python -m uvicorn tests.mock_services:app --port 9000
```

This runs a single FastAPI app on port 9000 that emulates:
- Mattermost API at `http://localhost:9000/mattermost/`
- Loomio API at `http://localhost:9000/loomio/`
- Decision Recorder at `http://localhost:9000/decision-recorder/`
- Member provisioner at `http://localhost:9000/provisioner/`

All responses are canned (no real data). All requests are logged in memory for inspection.

### Step 2: Configure environment

```bash
# Copy the test environment template
cp tests/env.test .env

# Edit .env — the defaults point at the mock services on port 9000.
# For integration tests with a REAL Claude API call, replace ANTHROPIC_API_KEY
# with your actual key. For unit-level testing, the test key is fine.
```

### Step 3: Run the Cooperative API

```bash
python -m uvicorn main:app --port 8000 --reload
```

The API is now running at `http://localhost:8000`. Health check: `curl http://localhost:8000/health`

### Step 4: Send a test webhook

```bash
curl -X POST http://localhost:8000/webhook/mattermost \
-H "Content-Type: application/json" \
-d '{
"token": "test_webhook_token_do_not_use_in_production",
"team_id": "test_team",
"team_domain": "test",
"channel_id": "test_channel",
"channel_name": "town-square",
"timestamp": 1700000000,
"user_id": "member_001",
"user_name": "alice",
"post_id": "post_001",
"text": "@clerk What proposals are open?",
"trigger_word": "@clerk"
}'
```

**With a real ANTHROPIC_API_KEY:** this will make a real Claude API call, the Clerk agent will run its tool-use loop, call the mock Loomio API for proposals, and return a response.

**With the test key:** this will fail at the Anthropic API call (expected — see "Fully mocked testing" below).

## Fully mocked testing (no API keys needed)

For CI and for testing without any API keys, the existing unit tests in `tests/` mock the Anthropic client at the Python level. To add new tests that exercise the full webhook → agent → tool flow without real API calls:

```python
# tests/test_integration.py
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
import os

# Set required env vars BEFORE importing main
os.environ["MATTERMOST_OUTGOING_WEBHOOK_TOKEN"] = "test_token"
os.environ["MATTERMOST_BOT_USER_ID"] = "bot_001"
os.environ["LOOMIO_WEBHOOK_SECRET"] = "test_secret"
os.environ["ANTHROPIC_API_KEY"] = "test_key"

from main import app

client = TestClient(app)


def test_health():
response = client.get("/health")
assert response.status_code == 200
assert response.json()["status"] == "ok"


def test_webhook_rejects_bad_token():
response = client.post("/webhook/mattermost", json={
"token": "wrong_token",
"team_id": "t", "team_domain": "t",
"channel_id": "c", "channel_name": "c",
"timestamp": 0, "user_id": "u",
"user_name": "u", "post_id": "p",
"text": "hello", "trigger_word": ""
})
assert response.status_code == 403


def test_webhook_ignores_bot_messages():
response = client.post("/webhook/mattermost", json={
"token": "test_token",
"team_id": "t", "team_domain": "t",
"channel_id": "c", "channel_name": "c",
"timestamp": 0, "user_id": "bot_001", # same as BOT_USER_ID
"user_name": "clerk", "post_id": "p",
"text": "hello", "trigger_word": ""
})
assert response.status_code == 200
assert response.json() == {} # empty = ignored
```

## Testing with the upstream OpenClaw Gateway

Once the upstream [OpenClaw](https://github.com/openclaw/openclaw) is installed alongside this API, the full integration test is:

1. Run the upstream OpenClaw Gateway (their docker-compose, port 18789)
2. Run the Iskander Cooperative API (this service, port 8000)
3. Run mock external services (port 9000) OR real Loomio + Decision Recorder
4. Install Iskander-specific skills into OpenClaw that route to `http://cooperative-api:8000`
5. Send a message through a connected channel (Mattermost) and verify the full flow

This full-stack integration test is a Phase C milestone (M-C4 in the NLnet application). The skill-bridge design is tracked in [Discussion #190](https://github.com/Argocyte/Iskander/discussions/190).

## What each test level covers

| Level | What it tests | External deps | When to run |
|---|---|---|---|
| Unit tests (`pytest tests/`) | Individual tools, Glass Box enforcement, write-tool guards | None (all mocked) | Every commit |
| Local dev (mock services) | Full webhook → agent → tool flow | Mock services on port 9000 + real or mock Claude API | During development |
| Integration (with upstream OpenClaw) | Channel → Gateway → Skill → API → external services | Upstream OpenClaw + this API + real or mock externals | Pre-deployment |
| Pilot (Trans Lives Housing Coop) | Real cooperative members using the full system | Everything real | Milestone M-C8 |
33 changes: 33 additions & 0 deletions src/IskanderOS/openclaw/tests/env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# OpenClaw Test Environment
# Copy to .env or export these before running OpenClaw locally.
# These point at the mock services running on port 9000.

# Required — Mattermost webhook authentication
MATTERMOST_OUTGOING_WEBHOOK_TOKEN=test_webhook_token_do_not_use_in_production
MATTERMOST_BOT_USER_ID=bot_user_test_001

# Required — Loomio webhook signature verification
LOOMIO_WEBHOOK_SECRET=test_loomio_secret_do_not_use_in_production

# Required — Claude API (use your real key for integration tests,
# or set to "test_key" for unit tests that mock the Anthropic client)
ANTHROPIC_API_KEY=test_key_replace_with_real_for_integration

# Optional — Mattermost bot posting (governance channel bridge)
MATTERMOST_BOT_TOKEN=test_bot_token
MATTERMOST_GOVERNANCE_CHANNEL_ID=mock_governance_channel
MATTERMOST_URL=http://localhost:9000/mattermost

# Optional — Loomio API base
LOOMIO_URL=http://localhost:9000/loomio

# Optional — Decision Recorder base
DECISION_RECORDER_URL=http://localhost:9000/decision-recorder

# Optional — Member provisioner
PROVISIONER_URL=http://localhost:9000/provisioner

# Optional — Clerk model (defaults to haiku for cost efficiency in testing)
CLERK_MODEL=claude-haiku-4-5-20251001
CLERK_MAX_TOKENS=2048
CLERK_RATE_LIMIT_PER_MINUTE=100
Loading
Loading