Skip to content

Commit e08931a

Browse files
CI + tests were added
1 parent c69acc3 commit e08931a

14 files changed

Lines changed: 207 additions & 32 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
name: CI
2+
3+
# Runs on every PR (and on pushes to the default branches, so a merge that
4+
# bypasses PR checks — e.g. an admin merge — still gets caught). See
5+
# ruff.toml for why the lint job's rule set is deliberately narrow, and
6+
# tests/conftest.py for why the test job needs a real Postgres service
7+
# rather than a mock.
8+
9+
on:
10+
pull_request:
11+
push:
12+
# TODO: remove "documents" once the CI setup itself has been verified —
13+
# it's here temporarily so pushes to this feature branch (with no PR
14+
# open yet) also trigger a run for testing.
15+
branches: [master, main, documents]
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
lint:
22+
name: Ruff
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
27+
- uses: actions/setup-python@v5
28+
with:
29+
python-version: "3.11"
30+
31+
- name: Install dev dependencies
32+
run: pip install -r requirements-dev.txt
33+
34+
- name: Ruff check
35+
run: ruff check .
36+
37+
test:
38+
name: Pytest
39+
runs-on: ubuntu-latest
40+
41+
# tests/conftest.py requires a real PostgreSQL instance — it creates the
42+
# aistudio_test database on first run and truncates tables between
43+
# tests. A service container is the GitHub Actions equivalent of the
44+
# `docker compose up -d postgres` step in local development.
45+
services:
46+
postgres:
47+
image: postgres:15-alpine
48+
env:
49+
POSTGRES_USER: aistudio
50+
POSTGRES_PASSWORD: aistudio
51+
POSTGRES_DB: aistudio
52+
ports:
53+
- 5432:5432
54+
options: >-
55+
--health-cmd "pg_isready -U aistudio"
56+
--health-interval 5s
57+
--health-timeout 3s
58+
--health-retries 5
59+
60+
env:
61+
# Matches the "pytest directly on the host machine" case documented at
62+
# the top of tests/conftest.py — the service container is reachable on
63+
# localhost:5432 from the job's steps.
64+
POSTGRES_HOST: localhost
65+
POSTGRES_PORT: 5432
66+
POSTGRES_USERNAME: aistudio
67+
POSTGRES_PASSWORD: aistudio
68+
POSTGRES_DATABASE: aistudio_test
69+
RABBITMQ_URL: localhost
70+
RABBITMQ_USERNAME: aistudio
71+
RABBITMQ_PASSWORD: aistudio
72+
73+
steps:
74+
- uses: actions/checkout@v4
75+
76+
- uses: actions/setup-python@v5
77+
with:
78+
python-version: "3.11"
79+
80+
- name: Install dev dependencies
81+
run: pip install -r requirements-dev.txt
82+
83+
- name: Run tests
84+
run: pytest tests/ -v

‎app/main.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from fastapi.responses import JSONResponse
44
from fastapi.exceptions import RequestValidationError
55

6+
from app.routers import system, ingest, benchmarks, results, jupyter, gpu_specs
7+
68
# The app instance
79
app = FastAPI(
810
title="AIStudio API",
@@ -60,8 +62,6 @@ async def generic_exception_handler(request: Request, exc: Exception):
6062
},
6163
)
6264

63-
from app.routers import system, ingest, benchmarks, results, jupyter, gpu_specs
64-
6565
# ── Routers ───────────────────────────────────────────────────────────────────
6666
app.include_router(system.router)
6767
app.include_router(ingest.router)

‎app/models/workload.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import datetime
44
from typing import Optional
55

6-
from sqlalchemy import DateTime, Enum, ForeignKey, Integer, String, Text, func
6+
from sqlalchemy import DateTime, Enum, Integer, String, Text, func
77
from sqlalchemy.dialects.postgresql import JSONB, UUID
88
from sqlalchemy.orm import Mapped, mapped_column, relationship
99

‎app/routers/benchmarks.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ async def get_benchmark_status(
7979

8080
return {
8181
"task_id": workload.workload_id,
82+
# Kept alongside task_id (same value) for callers that key off the
83+
# DB column name directly — e.g. Composer lifecycle-event tracking,
84+
# which correlates status polls against workload_id elsewhere.
85+
"workload_id": workload.workload_id,
8286
"state": workload.state,
8387
"error_message": workload.error_message,
8488
"updated_at": workload.updated_at,

‎app/services/node_inspector.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from app.config import settings
2-
import json
32
import logging
43
from app.services.ssh_executor import SSHExecutor
54

‎app/services/ssh_executor.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import re
22
import uuid
33
import paramiko
4-
from typing import Generator, Optional
4+
from typing import Optional
55

66
from app.database import SyncSessionLocal
77
from app.models.task_log import TaskLog

‎app/utils/sse.py‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,6 @@
88
from datetime import datetime
99
from typing import AsyncGenerator, Optional
1010

11-
# Masks the last two octets of any IPv4 address in log lines so node IPs are
12-
# never exposed to the client. e.g. 10.6.12.26 → 10.6.x.x
13-
_IP_RE = re.compile(r'(?<![.\d])(\d{1,3}\.\d{1,3})\.\d{1,3}\.\d{1,3}(?![.\d])')
14-
15-
16-
def _mask_ip(text: str) -> str:
17-
return _IP_RE.sub(r'\1.x.x', text)
18-
1911
from fastapi import Request
2012
from sqlalchemy import select
2113

@@ -24,6 +16,14 @@ def _mask_ip(text: str) -> str:
2416
from app.models.task_log import TaskLog
2517
from app.models.workload import Workload
2618

19+
# Masks the last two octets of any IPv4 address in log lines so node IPs are
20+
# never exposed to the client. e.g. 10.6.12.26 → 10.6.x.x
21+
_IP_RE = re.compile(r'(?<![.\d])(\d{1,3}\.\d{1,3})\.\d{1,3}\.\d{1,3}(?![.\d])')
22+
23+
24+
def _mask_ip(text: str) -> str:
25+
return _IP_RE.sub(r'\1.x.x', text)
26+
2727

2828
async def task_log_stream(
2929
workload_db_id: uuid.UUID,

‎pytest.ini‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
[pytest]
22
asyncio_mode = auto
3+
asyncio_default_fixture_loop_scope = session
4+
asyncio_default_test_loop_scope = session
35
testpaths = tests
6+
; Safety net: without this, a hung async test (e.g. an SSE/streaming
7+
; endpoint that never terminates) blocks the whole suite indefinitely
8+
; instead of failing fast.
9+
timeout = 60

‎requirements-dev.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@
55

66
pytest>=8.0.0
77
pytest-asyncio>=0.24.0
8+
pytest-timeout>=2.3.0
89
anyio[trio]>=4.0.0
10+
ruff>=0.16.0

‎ruff.toml‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Ruff configuration — see .github/workflows/ci.yml for how this runs in CI.
2+
#
3+
# Scope is deliberately narrow: syntax errors and Pyflakes (undefined names,
4+
# unused imports, etc.) rather than Ruff's full default rule set (which also
5+
# pulls in pyupgrade, flake8-bugbear, flake8-datetimez and a dozen other
6+
# plugins). Turning all of that on for a codebase that has never been linted
7+
# produces hundreds of pre-existing findings unrelated to any given PR, which
8+
# makes the check noise rather than a real gate. Start narrow and correct;
9+
# widen deliberately later if the team wants stricter style enforcement.
10+
target-version = "py311"
11+
12+
[lint]
13+
# E4/E7/E9 = pycodestyle error subset (real mistakes, not style nits).
14+
# F = Pyflakes (undefined names, unused imports, etc).
15+
select = ["E4", "E7", "E9", "F"]
16+
17+
[lint.per-file-ignores]
18+
# Models are imported here purely so Alembic's autogenerate can discover them
19+
# via `Base.metadata` — the names are never referenced directly in this file.
20+
"app/models/__init__.py" = ["F401"]
21+
22+
# SQLAlchemy 2.0 `Mapped["ClassName"]` relationship annotations use forward
23+
# references as plain strings resolved by SQLAlchemy at mapper-configuration
24+
# time, not by Python's own name resolution — Pyflakes has no way to know
25+
# that and flags them as undefined names. Known false positive; see
26+
# https://github.com/astral-sh/ruff/issues/8811.
27+
"app/models/*.py" = ["F821"]
28+
29+
# alembic/env.py intentionally star-imports every model so Alembic's
30+
# autogenerate can see them via Base.metadata; Pyflakes can't verify names
31+
# from a star import.
32+
"alembic/env.py" = ["F403"]
33+
34+
# Historical, already-applied migrations are immutable — don't hand-edit a
35+
# shipped revision just to satisfy a linter added long after it merged.
36+
"alembic/versions/*.py" = ["F401"]
37+
38+
# conftest.py must set POSTGRES_*/RABBITMQ_* environment variables BEFORE
39+
# importing app.config, since pydantic-settings reads env vars at class-body
40+
# time. That requires imports after statements — intentional, not an oversight.
41+
"tests/conftest.py" = ["E402"]

0 commit comments

Comments
 (0)