Files
app/backend/tests/api/routes/test_flavors.py
T
stroblmeandClaude Opus 5 d01a8dad37 Rename Installation to Instance
Follows the portal: the noun is "instance" everywhere the app says it —
UI strings, CLI output, error details, docs and comments. The wire keys
(`instance_id`, `instance_token`) and the hub route this calls move with it.

An existing cloud.json is adopted rather than refused: without the key
alias the dataclass fails to parse, which the caller swallows and reads as
"never enrolled" instead of "reconnect".

`instance_key` on a node type becomes `target_key`. It means the outside
thing a node points at, which is a different sense of the word, and keeping
both would put two meanings of "instance" in one codebase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
2026-08-31 10:12:01 +02:00

181 lines
6.0 KiB
Python

"""Named sizes: storing them, and what happens to a node that asks for one."""
import pytest
from fastapi.testclient import TestClient
from sqlmodel import Session, select
from fluksio.core.config import settings
from fluksio.core.db import seed_flavors
from fluksio.models import Flavor
def test_an_instance_starts_with_sizes_to_pick_from(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
response = client.get(
f"{settings.API_V1_STR}/flavors/", headers=superuser_token_headers
)
assert response.status_code == 200
names = {row["name"] for row in response.json()["data"]}
assert {"small", "medium", "large", "gpu-small"} <= names
def test_seeding_leaves_an_instance_that_has_its_own_alone(db: Session) -> None:
"""Re-adding a size somebody deliberately removed is an argument to avoid."""
before = {row.name for row in db.exec(select(Flavor)).all()}
seed_flavors(db)
assert {row.name for row in db.exec(select(Flavor)).all()} == before
def test_a_size_can_be_added_changed_and_removed(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
created = client.post(
f"{settings.API_V1_STR}/flavors/",
headers=superuser_token_headers,
json={"name": "gpu-big", "cpus": 16, "gpus": 4, "ram": 65536},
)
assert created.status_code == 200
assert created.json()["gpus"] == 4
again = client.post(
f"{settings.API_V1_STR}/flavors/",
headers=superuser_token_headers,
json={"name": "gpu-big", "cpus": 1},
)
assert again.status_code == 409
changed = client.patch(
f"{settings.API_V1_STR}/flavors/gpu-big",
headers=superuser_token_headers,
json={"cpus": 32},
)
assert changed.status_code == 200
# Only what was sent: the rest of the size is untouched.
assert (changed.json()["cpus"], changed.json()["gpus"]) == (32, 4)
removed = client.delete(
f"{settings.API_V1_STR}/flavors/gpu-big", headers=superuser_token_headers
)
assert removed.status_code == 200
listed = client.get(
f"{settings.API_V1_STR}/flavors/", headers=superuser_token_headers
).json()
assert "gpu-big" not in {row["name"] for row in listed["data"]}
def test_changing_a_size_is_a_superusers(
client: TestClient, normal_user_token_headers: dict[str, str]
) -> None:
"""Reading is open — the node panel offers them — and writing is not."""
assert (
client.get(
f"{settings.API_V1_STR}/flavors/", headers=normal_user_token_headers
).status_code
== 200
)
refused = client.post(
f"{settings.API_V1_STR}/flavors/",
headers=normal_user_token_headers,
json={"name": "sneaky", "cpus": 99},
)
assert refused.status_code == 403
def test_a_name_nobody_could_type_is_refused(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
refused = client.post(
f"{settings.API_V1_STR}/flavors/",
headers=superuser_token_headers,
json={"name": "GPU Big!", "cpus": 1},
)
assert refused.status_code == 422
def test_a_flow_cannot_name_a_size_that_does_not_exist(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
"""Caught at the save, which covers the canvas and `fluksio sync` at once."""
response = client.put(
f"{settings.API_V1_STR}/flows/flavor_check",
headers=superuser_token_headers,
json={
"name": "flavor_check",
"version": 1,
"nodes": [
{"id": "fit", "type": "python", "resources": {"flavor": "imaginary"}}
],
},
)
assert response.status_code == 422
assert "imaginary" in str(response.json()["detail"])
def test_a_size_a_node_still_asks_for_is_not_deleted(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
client.post(
f"{settings.API_V1_STR}/flavors/",
headers=superuser_token_headers,
json={"name": "in-use", "cpus": 2},
)
client.put(
f"{settings.API_V1_STR}/flows/flavor_user",
headers=superuser_token_headers,
json={
"name": "flavor_user",
"version": 1,
"nodes": [
{"id": "fit", "type": "python", "resources": {"flavor": "in-use"}}
],
},
)
try:
refused = client.delete(
f"{settings.API_V1_STR}/flavors/in-use", headers=superuser_token_headers
)
assert refused.status_code == 409
assert "flavor_user.fit" in refused.json()["detail"]
finally:
client.delete(
f"{settings.API_V1_STR}/flows/flavor_user", headers=superuser_token_headers
)
client.delete(
f"{settings.API_V1_STR}/flavors/in-use", headers=superuser_token_headers
)
def test_a_named_size_resolves_to_the_numbers_stored_for_it(db: Session) -> None:
from fluksio.flow.resources import UnknownFlavor, resolve_flavor
from fluksio.flow.schemas import Resources
resolved = resolve_flavor(Resources(flavor="gpu-small", duration_s=60))
seeded = db.get(Flavor, "gpu-small")
assert (resolved.cpus, resolved.gpus, resolved.ram) == (
seeded.cpus,
seeded.gpus,
seeded.ram,
)
# What the node said about itself survives; the flavor only sizes it.
assert resolved.duration_s == 60
assert resolved.flavor is None
# Read at build time, so editing a flavor edits what the next run gets.
seeded.cpus = 12
db.add(seeded)
db.commit()
assert resolve_flavor(Resources(flavor="gpu-small")).cpus == 12
with pytest.raises(UnknownFlavor, match="does not exist"):
resolve_flavor(Resources(flavor="never-stored"))
def test_declaring_nothing_never_looks_anything_up() -> None:
"""The common node pays nothing for a feature it does not use."""
from fluksio.flow.resources import resolve_flavor
from fluksio.flow.schemas import Resources
wanted = Resources(cpus=2)
assert resolve_flavor(wanted) is wanted