chore: initialize monorepo with project scaffolding, configs, and CI setup
This commit is contained in:
77
apps/coordinator-api/tests/test_client_receipts.py
Normal file
77
apps/coordinator-api/tests/test_client_receipts.py
Normal file
@ -0,0 +1,77 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from nacl.signing import SigningKey
|
||||
|
||||
from app.main import create_app
|
||||
from app.models import JobCreate, MinerRegister, JobResultSubmit
|
||||
from app.storage.db import init_db
|
||||
from app.config import settings
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def test_client(tmp_path_factory):
|
||||
db_file = tmp_path_factory.mktemp("data") / "client_receipts.db"
|
||||
settings.database_url = f"sqlite:///{db_file}"
|
||||
init_db()
|
||||
app = create_app()
|
||||
with TestClient(app) as client:
|
||||
yield client
|
||||
|
||||
|
||||
def test_receipt_endpoint_returns_signed_receipt(test_client: TestClient):
|
||||
signing_key = SigningKey.generate()
|
||||
settings.receipt_signing_key_hex = signing_key.encode().hex()
|
||||
|
||||
# register miner
|
||||
resp = test_client.post(
|
||||
"/v1/miners/register",
|
||||
json={"capabilities": {"price": 1}, "concurrency": 1},
|
||||
headers={"X-Api-Key": "miner_dev_key_1"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
# submit job
|
||||
job_payload = {
|
||||
"payload": {"task": "receipt"},
|
||||
}
|
||||
resp = test_client.post(
|
||||
"/v1/jobs",
|
||||
json=job_payload,
|
||||
headers={"X-Api-Key": "client_dev_key_1"},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
job_id = resp.json()["job_id"]
|
||||
|
||||
# poll for job assignment
|
||||
poll_resp = test_client.post(
|
||||
"/v1/miners/poll",
|
||||
json={"max_wait_seconds": 1},
|
||||
headers={"X-Api-Key": "miner_dev_key_1"},
|
||||
)
|
||||
assert poll_resp.status_code in (200, 204)
|
||||
|
||||
# submit result
|
||||
result_payload = {
|
||||
"result": {"units": 1, "unit_type": "gpu_seconds", "price": 1},
|
||||
"metrics": {"units": 1, "duration_ms": 500}
|
||||
}
|
||||
result_resp = test_client.post(
|
||||
f"/v1/miners/{job_id}/result",
|
||||
json=result_payload,
|
||||
headers={"X-Api-Key": "miner_dev_key_1"},
|
||||
)
|
||||
assert result_resp.status_code == 200
|
||||
signed_receipt = result_resp.json()["receipt"]
|
||||
assert signed_receipt["signature"]["alg"] == "Ed25519"
|
||||
|
||||
# fetch receipt via client endpoint
|
||||
receipt_resp = test_client.get(
|
||||
f"/v1/jobs/{job_id}/receipt",
|
||||
headers={"X-Api-Key": "client_dev_key_1"},
|
||||
)
|
||||
assert receipt_resp.status_code == 200
|
||||
payload = receipt_resp.json()
|
||||
assert payload["receipt_id"] == signed_receipt["receipt_id"]
|
||||
assert payload["signature"]["alg"] == "Ed25519"
|
||||
|
||||
settings.receipt_signing_key_hex = None
|
||||
57
apps/coordinator-api/tests/test_jobs.py
Normal file
57
apps/coordinator-api/tests/test_jobs.py
Normal file
@ -0,0 +1,57 @@
|
||||
import pytest
|
||||
from sqlmodel import Session, delete
|
||||
|
||||
from app.domain import Job, Miner
|
||||
from app.models import JobCreate
|
||||
from app.services.jobs import JobService
|
||||
from app.storage.db import init_db, session_scope
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def _init_db(tmp_path_factory):
|
||||
db_file = tmp_path_factory.mktemp("data") / "test.db"
|
||||
# override settings dynamically
|
||||
from app.config import settings
|
||||
|
||||
settings.database_url = f"sqlite:///{db_file}"
|
||||
init_db()
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def session():
|
||||
with session_scope() as sess:
|
||||
sess.exec(delete(Job))
|
||||
sess.exec(delete(Miner))
|
||||
sess.commit()
|
||||
yield sess
|
||||
|
||||
|
||||
def test_create_and_fetch_job(session: Session):
|
||||
svc = JobService(session)
|
||||
job = svc.create_job("client1", JobCreate(payload={"task": "noop"}))
|
||||
fetched = svc.get_job(job.id, client_id="client1")
|
||||
assert fetched.id == job.id
|
||||
assert fetched.payload["task"] == "noop"
|
||||
|
||||
|
||||
def test_acquire_next_job(session: Session):
|
||||
svc = JobService(session)
|
||||
job1 = svc.create_job("client1", JobCreate(payload={"n": 1}))
|
||||
job2 = svc.create_job("client1", JobCreate(payload={"n": 2}))
|
||||
|
||||
miner = Miner(id="miner1", capabilities={}, concurrency=1)
|
||||
session.add(miner)
|
||||
session.commit()
|
||||
|
||||
next_job = svc.acquire_next_job(miner)
|
||||
assert next_job is not None
|
||||
assert next_job.id == job1.id
|
||||
assert next_job.state == "RUNNING"
|
||||
|
||||
next_job2 = svc.acquire_next_job(miner)
|
||||
assert next_job2 is not None
|
||||
assert next_job2.id == job2.id
|
||||
|
||||
# No more jobs
|
||||
assert svc.acquire_next_job(miner) is None
|
||||
258
apps/coordinator-api/tests/test_miner_service.py
Normal file
258
apps/coordinator-api/tests/test_miner_service.py
Normal file
@ -0,0 +1,258 @@
|
||||
import pytest
|
||||
from sqlmodel import Session
|
||||
from nacl.signing import SigningKey
|
||||
|
||||
from aitbc_crypto.signing import ReceiptVerifier
|
||||
|
||||
from app.models import MinerRegister, JobCreate, Constraints
|
||||
from app.services.jobs import JobService
|
||||
from app.services.miners import MinerService
|
||||
from app.services.receipts import ReceiptService
|
||||
from app.storage.db import init_db, session_scope
|
||||
from app.config import settings
|
||||
from app.domain import JobReceipt
|
||||
from sqlmodel import select
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def _init_db(tmp_path_factory):
|
||||
db_file = tmp_path_factory.mktemp("data") / "miner.db"
|
||||
from app.config import settings
|
||||
|
||||
settings.database_url = f"sqlite:///{db_file}"
|
||||
init_db()
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def session():
|
||||
with session_scope() as sess:
|
||||
yield sess
|
||||
|
||||
|
||||
def test_register_and_poll_inflight(session: Session):
|
||||
miner_service = MinerService(session)
|
||||
job_service = JobService(session)
|
||||
|
||||
miner_service.register(
|
||||
"miner-1",
|
||||
MinerRegister(
|
||||
capabilities={"gpu": False},
|
||||
concurrency=1,
|
||||
),
|
||||
)
|
||||
|
||||
job_service.create_job("client-a", JobCreate(payload={"task": "demo"}))
|
||||
assigned = miner_service.poll("miner-1", max_wait_seconds=1)
|
||||
assert assigned is not None
|
||||
|
||||
miner = miner_service.get("miner-1")
|
||||
assert miner.inflight == 1
|
||||
|
||||
miner_service.release("miner-1")
|
||||
miner = miner_service.get("miner-1")
|
||||
assert miner.inflight == 0
|
||||
|
||||
|
||||
def test_heartbeat_updates_metadata(session: Session):
|
||||
miner_service = MinerService(session)
|
||||
|
||||
miner_service.register(
|
||||
"miner-2",
|
||||
MinerRegister(
|
||||
capabilities={"gpu": True},
|
||||
concurrency=2,
|
||||
),
|
||||
)
|
||||
|
||||
miner_service.heartbeat(
|
||||
"miner-2",
|
||||
payload=dict(inflight=3, status="BUSY", metadata={"load": 0.9}),
|
||||
)
|
||||
|
||||
miner = miner_service.get("miner-2")
|
||||
assert miner.status == "BUSY"
|
||||
assert miner.inflight == 3
|
||||
assert miner.extra_metadata.get("load") == 0.9
|
||||
|
||||
|
||||
def test_capability_constrained_assignment(session: Session):
|
||||
miner_service = MinerService(session)
|
||||
job_service = JobService(session)
|
||||
|
||||
miner = miner_service.register(
|
||||
"miner-cap",
|
||||
MinerRegister(
|
||||
capabilities={
|
||||
"gpus": [{"name": "NVIDIA RTX 4090", "memory_mb": 24576}],
|
||||
"models": ["stable-diffusion", "llama"]
|
||||
},
|
||||
concurrency=1,
|
||||
region="eu-west",
|
||||
),
|
||||
)
|
||||
|
||||
job_service.create_job(
|
||||
"client-x",
|
||||
JobCreate(
|
||||
payload={"task": "render"},
|
||||
constraints=Constraints(region="us-east"),
|
||||
),
|
||||
)
|
||||
job_service.create_job(
|
||||
"client-x",
|
||||
JobCreate(
|
||||
payload={"task": "render-hf"},
|
||||
constraints=Constraints(
|
||||
region="eu-west",
|
||||
gpu="NVIDIA RTX 4090",
|
||||
min_vram_gb=12,
|
||||
models=["stable-diffusion"],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assigned = miner_service.poll("miner-cap", max_wait_seconds=1)
|
||||
assert assigned is not None
|
||||
assert assigned.job_id is not None
|
||||
assert assigned.payload["task"] == "render-hf"
|
||||
|
||||
miner_state = miner_service.get("miner-cap")
|
||||
assert miner_state.inflight == 1
|
||||
|
||||
miner_service.release("miner-cap")
|
||||
|
||||
|
||||
def test_price_constraint(session: Session):
|
||||
miner_service = MinerService(session)
|
||||
job_service = JobService(session)
|
||||
|
||||
miner_service.register(
|
||||
"miner-price",
|
||||
MinerRegister(
|
||||
capabilities={
|
||||
"gpus": [{"name": "NVIDIA RTX 3070", "memory_mb": 8192}],
|
||||
"models": [],
|
||||
"price": 3.5,
|
||||
},
|
||||
concurrency=1,
|
||||
),
|
||||
)
|
||||
|
||||
job_service.create_job(
|
||||
"client-y",
|
||||
JobCreate(
|
||||
payload={"task": "cheap"},
|
||||
constraints=Constraints(max_price=2.0),
|
||||
),
|
||||
)
|
||||
job_service.create_job(
|
||||
"client-y",
|
||||
JobCreate(
|
||||
payload={"task": "fair"},
|
||||
constraints=Constraints(max_price=4.0),
|
||||
),
|
||||
)
|
||||
|
||||
assigned = miner_service.poll("miner-price", max_wait_seconds=1)
|
||||
assert assigned is not None
|
||||
assert assigned.payload["task"] == "fair"
|
||||
|
||||
miner_service.release("miner-price")
|
||||
|
||||
|
||||
def test_receipt_signing(session: Session):
|
||||
signing_key = SigningKey.generate()
|
||||
settings.receipt_signing_key_hex = signing_key.encode().hex()
|
||||
|
||||
job_service = JobService(session)
|
||||
miner_service = MinerService(session)
|
||||
receipt_service = ReceiptService(session)
|
||||
|
||||
miner_service.register(
|
||||
"miner-r",
|
||||
MinerRegister(
|
||||
capabilities={"price": 1.0},
|
||||
concurrency=1,
|
||||
),
|
||||
)
|
||||
|
||||
job = job_service.create_job(
|
||||
"client-r",
|
||||
JobCreate(payload={"task": "sign"}),
|
||||
)
|
||||
|
||||
receipt = receipt_service.create_receipt(
|
||||
job,
|
||||
"miner-r",
|
||||
{"units": 1.0, "unit_type": "gpu_seconds", "price": 1.2},
|
||||
{"units": 1.0},
|
||||
)
|
||||
|
||||
assert receipt is not None
|
||||
signature = receipt.get("signature")
|
||||
assert signature is not None
|
||||
assert signature["alg"] == "Ed25519"
|
||||
|
||||
miner_service.release("miner-r", success=True, duration_ms=500, receipt_id=receipt["receipt_id"])
|
||||
miner_state = miner_service.get("miner-r")
|
||||
assert miner_state.jobs_completed == 1
|
||||
assert miner_state.total_job_duration_ms == 500
|
||||
assert miner_state.average_job_duration_ms == 500
|
||||
assert miner_state.last_receipt_id == receipt["receipt_id"]
|
||||
|
||||
verifier = ReceiptVerifier(signing_key.verify_key.encode())
|
||||
payload = {k: v for k, v in receipt.items() if k not in {"signature", "attestations"}}
|
||||
assert verifier.verify(payload, receipt["signature"]) is True
|
||||
|
||||
# Reset signing key for subsequent tests
|
||||
settings.receipt_signing_key_hex = None
|
||||
|
||||
|
||||
def test_receipt_signing_with_attestation(session: Session):
|
||||
signing_key = SigningKey.generate()
|
||||
attest_key = SigningKey.generate()
|
||||
settings.receipt_signing_key_hex = signing_key.encode().hex()
|
||||
settings.receipt_attestation_key_hex = attest_key.encode().hex()
|
||||
|
||||
job_service = JobService(session)
|
||||
miner_service = MinerService(session)
|
||||
receipt_service = ReceiptService(session)
|
||||
|
||||
miner_service.register(
|
||||
"miner-attest",
|
||||
MinerRegister(capabilities={"price": 1.0}, concurrency=1),
|
||||
)
|
||||
|
||||
job = job_service.create_job(
|
||||
"client-attest",
|
||||
JobCreate(payload={"task": "attest"}),
|
||||
)
|
||||
|
||||
receipt = receipt_service.create_receipt(
|
||||
job,
|
||||
"miner-attest",
|
||||
{"units": 1.0, "unit_type": "gpu_seconds", "price": 2.0},
|
||||
{"units": 1.0},
|
||||
)
|
||||
|
||||
assert receipt is not None
|
||||
assert receipt.get("signature") is not None
|
||||
attestations = receipt.get("attestations")
|
||||
assert attestations is not None and len(attestations) == 1
|
||||
|
||||
stored_receipts = session.exec(select(JobReceipt).where(JobReceipt.job_id == job.id)).all()
|
||||
assert len(stored_receipts) == 1
|
||||
assert stored_receipts[0].receipt_id == receipt["receipt_id"]
|
||||
|
||||
payload = {k: v for k, v in receipt.items() if k not in {"signature", "attestations"}}
|
||||
|
||||
miner_verifier = ReceiptVerifier(signing_key.verify_key.encode())
|
||||
assert miner_verifier.verify(payload, receipt["signature"]) is True
|
||||
|
||||
attest_verifier = ReceiptVerifier(attest_key.verify_key.encode())
|
||||
assert attest_verifier.verify(payload, attestations[0]) is True
|
||||
|
||||
settings.receipt_signing_key_hex = None
|
||||
settings.receipt_attestation_key_hex = None
|
||||
|
||||
Reference in New Issue
Block a user