Skip to content
Merged
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
10 changes: 5 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ cd pi/backend && pip install -r requirements.txt -r requirements-dev.txt && pyte

CI runs both suites via `.github/workflows/backend-tests.yml` on changes under `cloud/backend/**` or `pi/backend/**`.

**Pi frontend**: `cd pi/frontend && npm ci && npm test` (Vitest; no Docker required)
**Pi frontend**: `cd pi/frontend && ../../scripts/npm.sh ci && npm test` (Vitest; no Docker required)

**Cloud frontend**: `cd cloud/frontend && npm ci && npm test` (TypeScript; run `npm run typecheck` before build)
**Cloud frontend**: `cd cloud/frontend && ../../scripts/npm.sh ci && npm test` (TypeScript; run `npm run typecheck` before build)

With coverage report:

```bash
cd pi/frontend && npm ci && npm run test:coverage
cd cloud/frontend && npm ci && npm run test:coverage
cd pi/frontend && ../../scripts/npm.sh ci && npm run test:coverage
cd cloud/frontend && ../../scripts/npm.sh ci && npm run test:coverage
cd cloud/frontend && npm run typecheck
```

Expand All @@ -82,7 +82,7 @@ CI runs Ruff and ESLint via `.github/workflows/lint.yml`. Run the same checks lo
npm run lint # same as ./scripts/lint.sh
```

Requires `python3 -m pip install ruff`, `npm ci` at repo root, and `npm ci` in `cloud/frontend` and `pi/frontend` before ESLint runs.
Requires `python3 -m pip install ruff`, `./scripts/npm.sh ci` at repo root, and `./scripts/npm.sh ci` in `cloud/frontend` and `pi/frontend` before ESLint runs. Use `./scripts/npm.sh` instead of `npm` when installing dependencies to avoid the deprecated `devdir` config warning in some environments.

### Cloud frontend TypeScript and OpenAPI

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ cd pi/backend && pip install -r requirements.txt -r requirements-dev.txt && pyth
Frontend (Vitest):

```bash
cd cloud/frontend && npm ci && npm test
cd pi/frontend && npm ci && npm test
cd cloud/frontend && ../../scripts/npm.sh ci && npm test
cd pi/frontend && ../../scripts/npm.sh ci && npm test
```

See [AGENTS.md](AGENTS.md) for coverage commands and CI details.
Expand Down
7 changes: 7 additions & 0 deletions cloud/backend/app/routers/articles.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ class ArticleMinimalRead(BaseModel):
organisation_id: int | None
is_addition: bool
is_active: bool
# DB FK is non-null; still optional here because category relation may be missing
# (orphaned FK / defensive path) and callers should not get a validation error.
article_category_id: int | None
article_category_name: str


class ArticleAdditionLinkIn(BaseModel):
Expand Down Expand Up @@ -109,13 +113,16 @@ class ArticleIngredientsRead(BaseModel):
def article_minimal_response(article: Article) -> ArticleMinimalRead:
category = article.article_category
organisation = category.organisation if category else None
category_id = category.id if category is not None else article.article_category_id
return ArticleMinimalRead(
id=article.id,
name=article.name,
label=article.label,
organisation_id=organisation.id if organisation else None,
is_addition=bool(article.is_addition),
is_active=bool(article.is_active),
article_category_id=category_id,
article_category_name=category.name if category else "",
)


Expand Down
49 changes: 48 additions & 1 deletion cloud/backend/tests/test_articles_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Articles API CRUD and addition links."""

from datetime import UTC, datetime
from types import SimpleNamespace

from app.database import SessionLocal
from app.main import app
Expand All @@ -18,6 +19,7 @@
User,
)
from app.roles import ROLE_TENANT_ADMIN
from app.routers.articles import article_minimal_response
from app.security import get_password_hash
from fastapi.testclient import TestClient

Expand All @@ -26,6 +28,40 @@
client = TestClient(app)


def test_article_minimal_response_allows_missing_category():
"""Minimal responses must not fail when the category relation is absent."""
orphan = SimpleNamespace(
id=99,
name="Orphan",
label="ORPH",
is_addition=False,
is_active=True,
article_category=None,
article_category_id=None,
)
row = article_minimal_response(orphan)
assert row.id == 99
assert row.article_category_id is None
assert row.article_category_name == ""
assert row.organisation_id is None


def test_article_minimal_response_keeps_category_id_when_relation_missing():
"""If the FK is present but the related row is gone, keep the id."""
dangling = SimpleNamespace(
id=100,
name="Dangling",
label="DANG",
is_addition=False,
is_active=True,
article_category=None,
article_category_id=7,
)
row = article_minimal_response(dangling)
assert row.article_category_id == 7
assert row.article_category_name == ""


def _seed_org_admin():
db = SessionLocal()
try:
Expand Down Expand Up @@ -167,9 +203,20 @@ def test_articles_crud_and_addition_links():
minimal = client.get(f"/articles/?organisation_id={org_id}&minimal=true", headers=headers)
assert minimal.status_code == 200, minimal.text
cola = next(a for a in minimal.json() if a["id"] == base_id)
assert set(cola.keys()) == {"id", "name", "label", "organisation_id", "is_addition", "is_active"}
assert set(cola.keys()) == {
"id",
"name",
"label",
"organisation_id",
"is_addition",
"is_active",
"article_category_id",
"article_category_name",
}
assert cola["name"] == "Cola"
assert cola["organisation_id"] == org_id
assert cola["article_category_id"] == cat_id
assert cola["article_category_name"]


def test_article_addition_links_round_trip_preselected():
Expand Down
19 changes: 18 additions & 1 deletion cloud/frontend/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -10435,6 +10435,21 @@
"is_active": {
"type": "boolean",
"title": "Is Active"
},
"article_category_id": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Article Category Id"
},
"article_category_name": {
"type": "string",
"title": "Article Category Name"
}
},
"type": "object",
Expand All @@ -10444,7 +10459,9 @@
"label",
"organisation_id",
"is_addition",
"is_active"
"is_active",
"article_category_id",
"article_category_name"
],
"title": "ArticleMinimalRead"
},
Expand Down
Loading
Loading