PowerTesting Docs
Back to PowerTesting

Setup Guide

This guide walks you through installing PowerTesting, configuring its SQLite database, seeding test cases from JSON, and adding new products / validators. It is the operator-facing companion to the Concepts and User Guides — if you are bringing up a fresh environment or onboarding a new product, start here.

Architecture

Components

PowerTesting ships as two cooperating services and a JSON seed corpus:

ComponentTechRole
testing-apiPython 3.11 / FastAPI / SQLAlchemy / SQLiteOwns the database. Exposes REST endpoints for products, test cases, runs, results, coverage, and user preferences.
testing-uiStatic HTML / ES modules / Bootstrap 5Browser SPA. Talks to testing-api over HTTPS. Open access — no sign-in.
testing-api/seed/JSON filesSource-of-truth for products, validator types, categories, and test cases. Loaded into SQLite on first start.

Repository Layout

PowerTesting/
├── testing-api/
│   ├── powertesting_api/        # FastAPI app (config, db, models, routers, seed)
│   ├── seed/
│   │   ├── products.json
│   │   ├── validator_types.json
│   │   ├── product_categories.json
│   │   └── test_cases/
│   │       ├── powersolver/*.json
│   │       ├── powerforecast/*.json
│   │       ├── powerdecision/*.json
│   │       ├── powerai/*.json
│   │       └── powerorchestrator/*.json
│   ├── scripts/
│   │   ├── seed_db.py           # CLI re-seed
│   │   ├── run_dev.sh           # Local dev runner
│   │   └── smoke_test.sh        # Post-deploy smoke
│   ├── data/
│   │   └── powertesting.db      # SQLite file (created on first start)
│   ├── docker-compose.yml
│   ├── Dockerfile
│   └── pyproject.toml
└── testing-ui/                  # Static SPA (deployed independently)

Quickstart (Docker)

The supported way to bring up the API is Docker Compose. From the repo root:

cd testing-api
docker compose up -d --build

# Verify
curl http://localhost:8090/health
# → {"status":"ok"}

# Confirm the seed loaded
curl http://localhost:8090/products | jq '.[].name'
# → "PowerSolver" "PowerForecast" "PowerDecision" "PowerAI" "PowerOrchestrator"

On first start the container creates /data/powertesting.db in the powertesting-data named volume, runs the SQLAlchemy schema migrations, and loads every JSON file under seed/.

Quickstart (Local Dev, no Docker)

For iterating on the API itself:

cd testing-api
python -m venv .venv
source .venv/bin/activate     # Windows: .venv\Scripts\Activate.ps1
pip install -e .

# Run the dev server (port 8090, hot-reload)
./scripts/run_dev.sh           # Windows: .\scripts\run_dev.bat

# Or directly
uvicorn powertesting_api.main:app --reload --port 8090

Configuration

Environment Variables

Every setting is read by powertesting_api/config.py with the POWERTESTING_ prefix. Defaults work for local dev; override in docker-compose.yml or a .env file for everything else.

VariableDefaultPurpose
POWERTESTING_API_PORT8090HTTP port the FastAPI app binds to.
POWERTESTING_DB_PATH./data/powertesting.dbAbsolute path to the SQLite file. In Docker we point this at the mounted volume (/data/powertesting.db).
POWERTESTING_ENVIRONMENTlocalFree-form label echoed by /health and stored on every run. Set to container or prod in compose.
POWERTESTING_SEED_ON_STARTUPtrueIf true, the app upserts every JSON in seed/ on each boot. Set false in prod after the initial load if you manage cases via API.
POWERTESTING_CORS_ORIGINS*Comma-separated list of allowed origins. Plain string, not JSON. See the CORS section.
POWERTESTING_BOOTSTRAP_ADMIN_USERNAMEadminUsername seeded at first start if no users exist.
POWERTESTING_BOOTSTRAP_ADMIN_PASSWORDemptyIf set, creates the admin row on first start. Empty = "do not seed an admin." See Auth Bootstrap.

CORS

POWERTESTING_CORS_ORIGINS is a comma-separated string — never a JSON array. pydantic-settings would try to json.loads() a JSON default and crash the container.

# dev: allow everything
POWERTESTING_CORS_ORIGINS=*

# prod: lock to the SPA origin
POWERTESTING_CORS_ORIGINS=https://testing.planningpowertools.com

Database

Where the DB Lives

  • Local dev: testing-api/data/powertesting.db (created on first start).
  • Docker: /data/powertesting.db inside the container, mounted from the named volume powertesting-data.

Move the volume between hosts to migrate data; copy the .db file out for backup. SQLite is single-file — no pg_dump or extra tooling needed.

Schema Overview

Every table is declared in powertesting_api/models.py:

TablePurpose
productsOne row per PDP product (PowerSolver, PowerForecast, …) with base URLs, supported levels, default timeout.
validator_typesCatalogue of validators, their JSON-Schema arg shapes, and which JS module implements them.
product_categoriesPer-product category buckets used by the UI to group cases.
test_casesThe full test corpus — one row per case, JSON columns for payload / validators / tags.
test_runsOne row per Run (a batch of cases executed against an environment).
test_resultsOne row per (run, case): status, durations, captured envelope, validation details.
user_preferencesSticky per-browser prefs (currently only the env switcher).
users / auth_tokensOperator auth for the prod-write gate (UI is open access, but a real user is needed to seed prod write actions).

Seed-on-Startup

With POWERTESTING_SEED_ON_STARTUP=true (the default), main.py calls load_all_seed() on app startup. This walks seed/ and upserts every JSON file. The startup log prints a summary:

Seeded: {'products': 5, 'validator_types': 28,
         'product_categories': 14, 'test_cases': 47}

Upserts are keyed: products by name, validator_types by name, test_cases by uuid. Test-case files without a uuid field are silently skipped — this is deliberate, so cases created at runtime via POST /test-cases aren't overwritten by an unrelated file.

Manual Re-seed

To re-seed without restarting (e.g. after editing a JSON file):

# From inside the container
docker exec -it powertesting-api python -m scripts.seed_db

# Or, in local dev
cd testing-api
python scripts/seed_db.py

Backup & Reset

# Backup (Docker)
docker run --rm -v powertesting-data:/data -v "$PWD":/backup \
  alpine sh -c 'cp /data/powertesting.db /backup/powertesting-$(date +%F).db'

# Wipe and re-seed from scratch
docker compose down -v          # removes the volume
docker compose up -d --build    # fresh DB, fresh seed
Heads up: down -v drops every test run and result you have ever recorded. If you care about run history, back up first.

Seed Files

Four kinds of file live under testing-api/seed/:

products.json

One object per product. Determines what shows up in the sidebar and where runs send their HTTP calls.

[
  {
    "name": "PowerSolver",
    "display_name": "PowerSolver",
    "color_hex": "#4E79A7",
    "icon": "cpu",
    "local_base_url": "http://localhost:8001",
    "prod_base_url":  "https://api.planningpowertools.com",
    "default_timeout_ms": 60000,
    "supports_levels": ["L1", "L2"],
    "display_order": 1,
    "enabled": true
  }
]

validator_types.json

Every validator the UI may reference must have a row here. product_scope is null for generic validators (StatusValidator, FieldMatchValidator, SchemaValidator, ResponseTimeValidator) and a product name otherwise.

{
  "name": "ScoreValidator",
  "description": "Validates Timefold solver score, e.g. '0hard/-5soft'.",
  "product_scope": "PowerSolver",
  "arg_schema": {
    "type": "object",
    "properties": {
      "pattern":  { "type": "string" },
      "feasible": { "type": "boolean" }
    },
    "required": ["pattern"]
  },
  "implementation_module": "products/powersolver.js"
}

product_categories.json

Per-product category buckets used to group cases in the UI (e.g. feasibility, regression, performance). Optional — cases also work without a declared category, though the UI groups them under "Uncategorised".

test_cases/<product>/*.json

One JSON file per test case, one folder per product. The seed loader walks the tree and upserts each file by its uuid.

Test Case Schema

Field Reference

FieldTypeRequiredNotes
uuidstring (UUID)yes (for seeding)Stable id. Re-using the same uuid upserts; missing uuid means the file is skipped by the seed loader.
namestringyesHuman-readable id (shown in the UI list).
productstringyesMust match a row in products.
levelL1 / L2 / AI_OWN / ORCH_OWNyesMust be in the product's supports_levels.
categorystringyesFree-form; should match a row in product_categories if you want UI grouping.
endpoint_templatestringyesUse {base} as a placeholder for the env-resolved base URL: {base}/api/solve.
http_methodstringno (default POST)Any HTTP verb.
request_payloadobjectyesBody sent to the endpoint. Echoed unchanged.
request_headersobjectnoExtra headers; auth is added by the run-controller.
validator_refsarrayyesList of { "name": "<ValidatorName>", "args": { … } }. Names must exist in validator_types.
tagsarray<string>noUsed by UI filters (smoke, regression, …).
environmentsarray<string>noIf empty / omitted, the case is environment-agnostic. Otherwise the run-controller only includes it for matching envs.
timeout_msintegerno (default 60000)Per-case override of the product's default_timeout_ms.
priorityintegerno (default 1)Higher = listed first.
enabledbooleanno (default true)Soft-disable without deleting.

Worked Example

A real PowerSolver smoke case — this is what seed/test_cases/powersolver/mock-feasible.json looks like:

{
  "uuid": "11111111-1111-1111-1111-111111111111",
  "name": "mock-feasible",
  "product": "PowerSolver",
  "level": "L1",
  "category": "feasibility",
  "endpoint_template": "{base}/api/solve",
  "http_method": "POST",
  "request_payload": { "problem": "mock" },
  "validator_refs": [
    { "name": "StatusValidator", "args": { "expected": "success" } },
    { "name": "ScoreValidator",  "args": { "maxSoftPenalty": 100 } }
  ],
  "tags": ["smoke"],
  "environments": ["local"]
}

Add a Test Case

You have two options:

1. File-based (recommended for cases you want under version control).

# Create the file
seed/test_cases/powersolver/my-new-case.json

# Generate a UUID and paste it into the "uuid" field
python -c "import uuid; print(uuid.uuid4())"

# Re-seed
docker exec -it powertesting-api python -m scripts.seed_db
# or restart the container — POWERTESTING_SEED_ON_STARTUP=true picks it up

2. Runtime (recommended for ad-hoc experimentation).

curl -X POST https://testing-api.planningpowertools.com/test-cases \
  -H "Content-Type: application/json" \
  -d @my-new-case.json

Runtime-created cases live only in the DB — they are not written back to disk. Promote them to the seed corpus when you want them version-controlled.

Add a Product

  1. Add a row to seed/products.json with name, base URLs, supported levels, and display order.
  2. (Optional) Add product-scoped validators to seed/validator_types.json.
  3. (Optional) Add the categories you'll use to seed/product_categories.json.
  4. Re-seed the DB (restart or run scripts/seed_db.py).
  5. On the UI side, create js/products/<new-product>.js exporting a module with the product's adapter and any product-specific validators, then register it in js/main.js.
  6. Add at least one test case in seed/test_cases/<new-product>/ so the product tab isn't empty.

Add a Validator Type

A validator has two halves: the metadata row and the JS implementation.

Metadata — add to seed/validator_types.json:

{
  "name": "MyCustomValidator",
  "description": "Asserts foo equals bar in the response payload.",
  "product_scope": "PowerSolver",
  "arg_schema": {
    "type": "object",
    "properties": { "expected": { "type": "string" } },
    "required": ["expected"]
  },
  "implementation_module": "products/powersolver.js"
}

Implementation — in the referenced JS module:

export const myCustomValidator = {
  name: 'MyCustomValidator',
  scope: 'PowerSolver',
  validate(envelope, args) {
    const actual = envelope.result?.foo;
    const ok = actual === args.expected;
    return {
      ok,
      message: ok
        ? `OK (foo == ${args.expected})`
        : `Expected foo=${args.expected}, got ${actual}`,
    };
  },
};

Then re-seed the DB (so the metadata row is registered) and reload the UI (so the JS implementation is picked up). Coverage view will now show the validator under its product scope.

Auth Bootstrap

The UI itself is open access, but operator-level write actions (run prod, set prod prefs) require a token. To create the first admin:

# In docker-compose.yml or a .env file
POWERTESTING_BOOTSTRAP_ADMIN_USERNAME=admin
POWERTESTING_BOOTSTRAP_ADMIN_PASSWORD=<a strong password>

# Then start the container — main.py calls bootstrap_admin_if_empty()
docker compose up -d

The password is hashed (stdlib scrypt) before being written. The bootstrap is a no-op if any user already exists or if the password env var is empty — this is deliberate so a default like change-me never ships.

Verifying Setup

The repository ships a smoke script that exercises the major endpoints:

cd testing-api
./scripts/smoke_test.sh http://localhost:8090

# Expected:
# ✓ /health
# ✓ /products       (5 rows)
# ✓ /validator-types (28 rows)
# ✓ /test-cases     (≥1 row)
# ✓ /test-runs      (POST, then GET)

If any line fails, jump to Troubleshooting.

Production Deploy

The cloud setup running at testing-api.planningpowertools.com is:

  • Hetzner host running Docker.
  • docker-compose.yml from testing-api/, with prod env overrides.
  • Caddy / nginx in front terminating TLS and forwarding to :8090.
  • powertesting-data named volume mounted at /data — this is the only stateful dependency.
# prod env overrides (in deploy .env)
POWERTESTING_ENVIRONMENT=prod
POWERTESTING_SEED_ON_STARTUP=true                         # safe: upserts only
POWERTESTING_CORS_ORIGINS=https://testing.planningpowertools.com
POWERTESTING_BOOTSTRAP_ADMIN_PASSWORD=<set once, then unset>

The static testing-ui is published separately to testing.planningpowertools.com. It reads the API base from window.POWERTESTING_API_URL in index.html — update that line if the API hostname ever changes.

Troubleshooting

SymptomLikely causeFix
Container restart-loops, log shows json.decoder.JSONDecodeErrorPOWERTESTING_CORS_ORIGINS set as a JSON array.Use a comma-separated string, e.g. https://a.com,https://b.com.
/products returns []Seeding is off, or seed/ wasn't copied into the image.Set POWERTESTING_SEED_ON_STARTUP=true; rebuild with --no-cache.
Test case file ignored by re-seedMissing uuid field.Add a UUID; the loader skips uuid-less files on purpose.
Run errors with "Validator not registered"validator_refs[].name doesn't match any row in validator_types.Add the row to validator_types.json or fix the name in the case.
Run errors with "Product 'X' does not support level 'L2'"Case's level isn't in the product's supports_levels.Either change the case's level or add the level to the product row in products.json.
UI shows every product as UNKNOWNCORS blocked, or the product API isn't running.Confirm the product container is up; widen POWERTESTING_CORS_ORIGINS; check the product's own CORS for the SPA origin.
DB file missing on hostVolume not mounted, or path mismatch with POWERTESTING_DB_PATH.Confirm volumes: entry in compose; POWERTESTING_DB_PATH=/data/powertesting.db.