From 36d86ef4c65b27507e2320ee912d4a56fcc3a5ee Mon Sep 17 00:00:00 2001 From: Andrey Cheptsov Date: Tue, 4 Aug 2026 15:57:55 +0200 Subject: [PATCH] Improve `dstack fleet` table readability and ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fleets table was hard to scan: every status rendered in the same neutral white, no column was dimmed, and fleets came back in arbitrary order because the `/fleets/list` endpoint had no `ORDER BY`. Colors now follow one rule — blue for occupied or in-flight, green for ready, red for broken, gold for degraded, dim for settled — and bold is reserved for statuses that need attention. Note that the previous color map was partly dead: `grey` is not a valid rich color, so rich silently dropped those styles, including the bold. Dimming distinguishes the two row types. A fleet row describes what was requested (name, resources, candidate backends, price cap) and is dimmed throughout, so it only draws the eye when its status is not `active`. An instance row shows what actually exists and is billed, and stays bright. Also drops the `-` placeholders in favour of blank cells, and formats the max price range as `$0..1` rather than `$0..$1`. Fleets are now sorted newest-first, both in the CLI so that it works against existing servers, and in `list_project_fleet_models` so the endpoint is deterministic for other clients. Co-Authored-By: Claude Opus 5 (1M context) --- src/dstack/_internal/cli/utils/fleet.py | 179 ++++++++++-------- .../_internal/server/services/fleets.py | 7 +- src/tests/_internal/cli/utils/test_fleet.py | 132 +++++++++++-- .../_internal/server/routers/test_fleets.py | 24 +++ 4 files changed, 246 insertions(+), 96 deletions(-) diff --git a/src/dstack/_internal/cli/utils/fleet.py b/src/dstack/_internal/cli/utils/fleet.py index 875e2ef436..811db7db9f 100644 --- a/src/dstack/_internal/cli/utils/fleet.py +++ b/src/dstack/_internal/cli/utils/fleet.py @@ -10,10 +10,34 @@ ) from dstack._internal.core.models.backends.base import BackendType from dstack._internal.core.models.fleets import Fleet, FleetNodesSpec, FleetStatus +from dstack._internal.core.models.health import HealthStatus from dstack._internal.core.models.instances import Instance, InstanceStatus from dstack._internal.core.models.resources import GPUSpec, ResourcesSpec from dstack._internal.utils.common import DateFormatter, pretty_date +# Status styles. Bold marks transient states and states that need attention. +# NOTE: "grey" is not a valid rich color — rich silently drops the whole style. Use "grey58". +_FLEET_STATUS_STYLES = { + FleetStatus.SUBMITTED: "grey58", + FleetStatus.ACTIVE: "grey58", + FleetStatus.TERMINATING: "bold deep_sky_blue1", + FleetStatus.TERMINATED: "grey58", + FleetStatus.FAILED: "bold indian_red1", +} +_INSTANCE_STATUS_STYLES = { + InstanceStatus.PENDING: "bold deep_sky_blue1", + InstanceStatus.PROVISIONING: "bold deep_sky_blue1", + InstanceStatus.IDLE: "bold sea_green3", + InstanceStatus.BUSY: "bold deep_sky_blue1", + InstanceStatus.TERMINATING: "bold deep_sky_blue1", + InstanceStatus.TERMINATED: "grey58", +} + + +def _dim(value: str) -> str: + """Renders a value as secondary, leaving blanks alone so they produce no markup.""" + return f"[secondary]{value}[/]" if value else "" + def print_fleets_table(fleets: List[Fleet], current_project: str, verbose: bool = False) -> None: console.print(get_fleets_table(fleets, current_project=current_project, verbose=verbose)) @@ -28,22 +52,27 @@ def get_fleets_table( ) -> Table: table = Table(box=None) - # Columns - table.add_column("NAME", style="bold", no_wrap=True) - table.add_column("NODES") + # Columns. A fleet row describes what was requested and is dimmed; an instance row shows + # what exists and stays bright, so the per-row-type columns are dimmed below instead. + # Bold is reserved for statuses that need attention. + table.add_column("NAME", no_wrap=True) + table.add_column("NODES", style="grey58") if verbose: table.add_column("RESOURCES") table.add_column("DRIVER") else: table.add_column("GPU") - table.add_column("SPOT") + table.add_column("SPOT", style="grey58") table.add_column("BACKEND") table.add_column("PRICE") table.add_column("STATUS", no_wrap=True) - table.add_column("CREATED", no_wrap=True) + table.add_column("CREATED", style="grey58", no_wrap=True) if verbose: table.add_column("ERROR") + # Most recently created fleets first. The `/fleets/list` endpoint returns them unordered. + fleets = sorted(fleets, key=lambda f: f.created_at, reverse=True) + for fleet in fleets: # Fleet row config = fleet.spec.configuration @@ -53,37 +82,39 @@ def get_fleets_table( if config.ssh_config is not None: # SSH fleet: fixed number of hosts, no cloud billing nodes = str(len(config.ssh_config.hosts)) - resources = "-" - gpu = "-" + resources = "" + gpu = "" backend = "ssh" - spot_policy = "-" - max_price = "-" + spot_policy = "" + max_price = "" else: # Backend fleet: dynamic nodes, cloud billing nodes = _format_nodes(config.nodes) - resources = config.resources.pretty_format() if config.resources else "-" + resources = config.resources.pretty_format() if config.resources else "" gpu = _format_fleet_gpu(config.resources) backend = _format_backends(config.backends) - spot_policy = "-" + spot_policy = "" if merged_profile and merged_profile.spot_policy: spot_policy = merged_profile.spot_policy.value - # Format as "$0..$X.XX" range, or "-" if not set + # Format as "$0..X.XX" range, or blank if not set if merged_profile and merged_profile.max_price is not None: - max_price = f"$0..{_format_price(merged_profile.max_price)}" + max_price = f"$0..{_format_amount(merged_profile.max_price)}" else: - max_price = "-" + max_price = "" # In verbose mode, append placement to nodes if cluster if verbose and config.placement and config.placement.value == "cluster": nodes = f"{nodes} (cluster)" + fleet_name = format_entity_reference(fleet.name, fleet.project_name, current_project) + # The whole fleet row is dimmed: it describes what was requested, not what exists fleet_row = { - "NAME": format_entity_reference(fleet.name, fleet.project_name, current_project), + "NAME": _dim(fleet_name), "NODES": nodes, - "RESOURCES": resources, - "GPU": gpu, - "BACKEND": backend, - "PRICE": max_price, + "RESOURCES": _dim(resources), + "GPU": _dim(gpu), + "BACKEND": _dim(backend), + "PRICE": _dim(max_price), "SPOT": spot_policy, "STATUS": _format_fleet_status(fleet), "CREATED": format_date(fleet.created_at), @@ -99,16 +130,18 @@ def get_fleets_table( # Format backend with region (and AZ in verbose mode) if verbose and instance.availability_zone: # In verbose mode, show AZ instead of region (AZ is more specific) - backend_with_region = format_backend(instance.backend, instance.availability_zone) + backend_with_region = _format_instance_backend( + instance.backend, instance.availability_zone + ) else: - backend_with_region = format_backend(instance.backend, instance.region) + backend_with_region = _format_instance_backend(instance.backend, instance.region) # Get spot info from instance resources (not applicable to SSH) if is_ssh_instance: - instance_spot = "-" - instance_price = "-" + instance_spot = "" + instance_price = "" else: - instance_spot = "-" + instance_spot = "" if ( instance.instance_type is not None and instance.instance_type.resources is not None @@ -124,7 +157,7 @@ def get_fleets_table( "RESOURCES": _format_instance_resources(instance), "GPU": _format_instance_gpu(instance), "BACKEND": backend_with_region, - "DRIVER": instance.gpu_driver.version if instance.gpu_driver else "-", + "DRIVER": instance.gpu_driver.version if instance.gpu_driver else "", "PRICE": instance_price, "SPOT": instance_spot, "STATUS": _format_instance_status(instance), @@ -134,7 +167,8 @@ def get_fleets_table( if instance.status == InstanceStatus.TERMINATED and instance.termination_reason: instance_row["ERROR"] = instance.termination_reason - add_row_from_dict(table, instance_row, style="secondary") + # No row-level dimming: only the columns styled above are dimmed on instance rows + add_row_from_dict(table, instance_row) return table @@ -142,7 +176,7 @@ def get_fleets_table( def _format_nodes(nodes: Optional[FleetNodesSpec]) -> str: """Format nodes spec as '0..1', '3', '2..10', etc.""" if nodes is None: - return "-" + return "" if nodes.min == nodes.max: return str(nodes.min) if nodes.max is None: @@ -150,6 +184,13 @@ def _format_nodes(nodes: Optional[FleetNodesSpec]) -> str: return f"{nodes.min}..{nodes.max}" +def _format_instance_backend(backend: Optional[BackendType], region: Optional[str]) -> str: + """Both the backend and its region are real values, so only the parentheses are dimmed.""" + if backend is None or not region: + return format_backend(backend, region) + return f"{format_backend(backend, None)} [secondary]([/]{region}[secondary])[/]" + + def _format_backends(backends: Optional[List[BackendType]]) -> str: if backends is None or len(backends) == 0: return "*" @@ -171,14 +212,14 @@ def _format_range(min_val: Optional[Any], max_val: Optional[Any]) -> str: def _format_fleet_gpu(resources: Optional[ResourcesSpec]) -> str: """Extract GPU-only info from fleet requirements, handling ranges.""" if resources is None or resources.gpu is None: - return "-" + return "" gpu: GPUSpec = resources.gpu # Check if there's actually a GPU requirement count = gpu.count if count is None or (count.min == 0 and (count.max is None or count.max == 0)): - return "-" + return "" parts = [] @@ -203,81 +244,65 @@ def _format_fleet_gpu(resources: Optional[ResourcesSpec]) -> str: def _format_fleet_status(fleet: Fleet) -> str: - status = fleet.status - status_text = status.value - - color_map = { - FleetStatus.SUBMITTED: "grey", - FleetStatus.ACTIVE: "white", - FleetStatus.TERMINATING: "deep_sky_blue1", - FleetStatus.TERMINATED: "grey", - FleetStatus.FAILED: "indian_red1", - } - color = color_map.get(status, "white") - is_finished = status in [FleetStatus.TERMINATED, FleetStatus.FAILED] - status_style = f"bold {color}" if not is_finished else color - return f"[{status_style}]{status_text}[/]" + style = _FLEET_STATUS_STYLES.get(fleet.status, "white") + return f"[{style}]{fleet.status.value}[/]" def _format_instance_status(instance: Instance) -> str: """Format instance status with colors and health info.""" status = instance.status - status_text = status.value + style = _INSTANCE_STATUS_STYLES.get(status, "white") total_blocks = instance.total_blocks - busy_blocks = instance.busy_blocks - if ( - status in [InstanceStatus.IDLE, InstanceStatus.BUSY] - and total_blocks is not None - and total_blocks > 1 - ): - status_text = f"{busy_blocks}/{total_blocks} {InstanceStatus.BUSY.value}" - - # Add health status - health_suffix = "" - if status in [InstanceStatus.IDLE, InstanceStatus.BUSY]: + if status.is_available() and total_blocks is not None and total_blocks > 1: + # Reads as " of busy": the fraction is a quantity, so it's + # dimmed, while the word keeps the color of the status (no busy blocks is still idle). + status_text = ( + f"[secondary]{instance.busy_blocks}/{total_blocks}[/]" + f" [{style}]{InstanceStatus.BUSY.value}[/]" + ) + else: + status_text = f"[{style}]{status.value}[/]" + + if status.is_available(): if instance.unreachable: - health_suffix = " (unreachable)" + status_text += " [bold indian_red1](unreachable)[/]" + elif instance.health_status == HealthStatus.WARNING: + status_text += " [bold gold1](warning)[/]" elif not instance.health_status.is_healthy(): - health_suffix = f" ({instance.health_status.value})" - - color_map = { - InstanceStatus.PENDING: "deep_sky_blue1", - InstanceStatus.PROVISIONING: "deep_sky_blue1", - InstanceStatus.IDLE: "sea_green3", - InstanceStatus.BUSY: "white", - InstanceStatus.TERMINATING: "deep_sky_blue1", - InstanceStatus.TERMINATED: "grey", - } - color = color_map.get(status, "white") - is_finished = status == InstanceStatus.TERMINATED - status_style = f"bold {color}" if not is_finished else color - return f"[{status_style}]{status_text}{health_suffix}[/]" + status_text += f" [bold indian_red1]({instance.health_status.value})[/]" + + return status_text + + +def _format_amount(price: float) -> str: + """Formats a price without a currency sign, trimming trailing zeros.""" + return f"{price:.4f}".rstrip("0").rstrip(".") def _format_price(price: Optional[float]) -> str: if price is None: - return "-" - return f"${price:.4f}".rstrip("0").rstrip(".") + return "" + return f"${_format_amount(price)}" def _format_instance_gpu(instance: Instance) -> str: if instance.instance_type is None: - return "-" + return "" if instance.backend == BackendType.REMOTE and instance.status in [ InstanceStatus.PENDING, InstanceStatus.PROVISIONING, ]: - return "-" - return instance.instance_type.resources.pretty_format(gpu_only=True, include_spot=False) or "-" + return "" + return instance.instance_type.resources.pretty_format(gpu_only=True, include_spot=False) def _format_instance_resources(instance: Instance) -> str: if instance.instance_type is None: - return "-" + return "" if instance.backend == BackendType.REMOTE and instance.status in [ InstanceStatus.PENDING, InstanceStatus.PROVISIONING, ]: - return "-" + return "" return instance.instance_type.resources.pretty_format(include_spot=False) diff --git a/src/dstack/_internal/server/services/fleets.py b/src/dstack/_internal/server/services/fleets.py index 2c6a924c50..f2967f6d85 100644 --- a/src/dstack/_internal/server/services/fleets.py +++ b/src/dstack/_internal/server/services/fleets.py @@ -343,7 +343,12 @@ async def list_project_fleet_models( options = [joinedload(FleetModel.project).load_only(ProjectModel.name)] if include_instances: options.append(selectinload(FleetModel.instances.and_(InstanceModel.deleted == False))) - res = await session.execute(select(FleetModel).where(*filters).options(*options)) + res = await session.execute( + select(FleetModel) + .where(*filters) + .order_by(FleetModel.created_at.desc(), FleetModel.id) + .options(*options) + ) return list(res.unique().scalars().all()) diff --git a/src/tests/_internal/cli/utils/test_fleet.py b/src/tests/_internal/cli/utils/test_fleet.py index 00fedff685..f8a6254758 100644 --- a/src/tests/_internal/cli/utils/test_fleet.py +++ b/src/tests/_internal/cli/utils/test_fleet.py @@ -18,6 +18,7 @@ SSHHostParams, SSHParams, ) +from dstack._internal.core.models.health import HealthStatus from dstack._internal.core.models.instances import ( Disk, Gpu, @@ -61,6 +62,14 @@ def get_table_cells(table: Table) -> list[dict[str, str]]: return rows +def get_table_cell_markup(table: Table, column_name: str, row_idx: int = 0) -> str: + """Returns the raw cell value, with rich markup intact.""" + for col in table.columns: + if str(col.header) == column_name and row_idx < len(col._cells): + return str(col._cells[row_idx]) + return "" + + def get_table_cell_style(table: Table, column_name: str, row_idx: int = 0) -> Optional[str]: for col in table.columns: if str(col.header) == column_name: @@ -233,7 +242,7 @@ def test_backend_fleet_without_verbose(self): assert fleet_row["NODES"] == "0..4" assert fleet_row["BACKEND"] == "aws" assert fleet_row["SPOT"] == "auto" - assert fleet_row["PRICE"] == "-" # no max_price set + assert fleet_row["PRICE"] == "" # no max_price set assert fleet_row["STATUS"] == "active" instance_row = cells[1] @@ -273,7 +282,7 @@ def test_backend_fleet_with_verbose(self): assert fleet_row["NODES"] == "1 (cluster)" assert fleet_row["BACKEND"] == "gcp" assert fleet_row["SPOT"] == "on-demand" - assert fleet_row["PRICE"] == "$0..$2" + assert fleet_row["PRICE"] == "$0..2" assert fleet_row["STATUS"] == "active" instance_row = cells[1] @@ -320,15 +329,15 @@ def test_ssh_fleet_without_verbose(self): assert fleet_row["NAME"] == "my-ssh" assert fleet_row["NODES"] == "2" assert fleet_row["BACKEND"] == "ssh" - assert fleet_row["SPOT"] == "-" - assert fleet_row["PRICE"] == "-" + assert fleet_row["SPOT"] == "" + assert fleet_row["PRICE"] == "" assert fleet_row["STATUS"] == "active" for i, instance_row in enumerate(cells[1:], start=0): assert f"instance={i}" in instance_row["NAME"] assert instance_row["BACKEND"] == "ssh" - assert instance_row["SPOT"] == "-" - assert instance_row["PRICE"] == "-" + assert instance_row["SPOT"] == "" + assert instance_row["PRICE"] == "" def test_ssh_fleet_with_verbose(self): instance = create_test_instance( @@ -354,16 +363,16 @@ def test_ssh_fleet_with_verbose(self): fleet_row = cells[0] assert fleet_row["NAME"] == "my-ssh" assert fleet_row["NODES"] == "1 (cluster)" - assert fleet_row["RESOURCES"] == "-" + assert fleet_row["RESOURCES"] == "" assert fleet_row["BACKEND"] == "ssh" - assert fleet_row["SPOT"] == "-" - assert fleet_row["PRICE"] == "-" + assert fleet_row["SPOT"] == "" + assert fleet_row["PRICE"] == "" instance_row = cells[1] assert "instance=0" in instance_row["NAME"] assert instance_row["BACKEND"] == "ssh" - assert instance_row["SPOT"] == "-" - assert instance_row["PRICE"] == "-" + assert instance_row["SPOT"] == "" + assert instance_row["PRICE"] == "" def test_mixed_fleets(self): backend_instance = create_test_instance( @@ -416,12 +425,12 @@ def test_mixed_fleets(self): assert cells[2]["NAME"] == "ssh-fleet" assert cells[2]["NODES"] == "1" assert cells[2]["BACKEND"] == "ssh" - assert cells[2]["SPOT"] == "-" - assert cells[2]["PRICE"] == "-" + assert cells[2]["SPOT"] == "" + assert cells[2]["PRICE"] == "" assert "instance=0" in cells[3]["NAME"] - assert cells[3]["SPOT"] == "-" - assert cells[3]["PRICE"] == "-" + assert cells[3]["SPOT"] == "" + assert cells[3]["PRICE"] == "" def test_fleet_status_colors(self): # Add instances to avoid placeholder rows affecting row indices @@ -441,8 +450,9 @@ def test_fleet_status_colors(self): [active_fleet, terminating_fleet], current_project="test-project", verbose=False ) + # Settled states are dimmed, so a fleet row only draws the eye when something is wrong active_style = get_table_cell_style(table, "STATUS", 0) - assert active_style == "bold white" + assert active_style == "grey58" # Row 2 (after active fleet's instance) terminating_style = get_table_cell_style(table, "STATUS", 2) @@ -463,7 +473,93 @@ def test_instance_status_colors(self): assert idle_style == "bold sea_green3" busy_style = get_table_cell_style(table, "STATUS", 2) - assert busy_style == "bold white" + assert busy_style == "bold deep_sky_blue1" + + def test_instance_status_blocks_keep_actual_status_color(self): + idle_instance = create_test_instance(instance_num=0, status=InstanceStatus.IDLE) + idle_instance.total_blocks = 4 + idle_instance.busy_blocks = 0 + busy_instance = create_test_instance(instance_num=1, status=InstanceStatus.BUSY) + busy_instance.total_blocks = 4 + busy_instance.busy_blocks = 2 + + fleet = create_backend_fleet(name="test", instances=[idle_instance, busy_instance]) + + table = get_fleets_table([fleet], current_project="test-project", verbose=False) + cells = get_table_cells(table) + + # The fraction is dimmed, the word keeps the color of the instance's actual status + assert cells[1]["STATUS"] == "0/4 busy" + assert ( + get_table_cell_markup(table, "STATUS", 1) + == "[secondary]0/4[/] [bold sea_green3]busy[/]" + ) + + assert cells[2]["STATUS"] == "2/4 busy" + assert ( + get_table_cell_markup(table, "STATUS", 2) + == "[secondary]2/4[/] [bold deep_sky_blue1]busy[/]" + ) + + def test_instance_health_suffix(self): + unreachable = create_test_instance(instance_num=0, status=InstanceStatus.IDLE) + unreachable.unreachable = True + warning = create_test_instance(instance_num=1, status=InstanceStatus.BUSY) + warning.health_status = HealthStatus.WARNING + failure = create_test_instance(instance_num=2, status=InstanceStatus.BUSY) + failure.health_status = HealthStatus.FAILURE + + fleet = create_backend_fleet(name="test", instances=[unreachable, warning, failure]) + + table = get_fleets_table([fleet], current_project="test-project", verbose=False) + cells = get_table_cells(table) + + assert cells[1]["STATUS"] == "idle (unreachable)" + assert "[bold indian_red1](unreachable)[/]" in get_table_cell_markup(table, "STATUS", 1) + + assert cells[2]["STATUS"] == "busy (warning)" + assert "[bold gold1](warning)[/]" in get_table_cell_markup(table, "STATUS", 2) + + assert cells[3]["STATUS"] == "busy (failure)" + assert "[bold indian_red1](failure)[/]" in get_table_cell_markup(table, "STATUS", 3) + + def test_fleet_resources_dimmed_instance_resources_not(self): + instance = create_test_instance(instance_num=0, status=InstanceStatus.IDLE) + fleet = create_backend_fleet( + name="test", gpu_count_min=1, gpu_count_max=2, max_price=2.0, instances=[instance] + ) + + table = get_fleets_table([fleet], current_project="test-project", verbose=False) + + # The fleet row describes a requirement, so name, GPU and the price cap are dimmed + assert get_table_cell_markup(table, "NAME", 0) == "[secondary]test[/]" + assert get_table_cell_markup(table, "GPU", 0) == "[secondary]gpu:1..2[/]" + assert get_table_cell_markup(table, "PRICE", 0) == "[secondary]$0..2[/]" + # The instance shows what actually got provisioned and billed, so it stays bright + for column in ["NAME", "GPU", "PRICE"]: + assert "secondary" not in get_table_cell_markup(table, column, 1), column + # The backend and its region are both real values; only the parentheses are dimmed + assert ( + get_table_cell_markup(table, "BACKEND", 1) + == "aws [secondary]([/]us-east-1[secondary])[/]" + ) + # No row-level style dims the rest of the instance row + assert table.rows[1].style is None + + def test_fleets_sorted_by_created_at_desc(self): + oldest = create_backend_fleet(name="oldest") + oldest.created_at = datetime(2023, 1, 1, tzinfo=timezone.utc) + newest = create_backend_fleet(name="newest") + newest.created_at = datetime(2023, 3, 1, tzinfo=timezone.utc) + middle = create_backend_fleet(name="middle") + middle.created_at = datetime(2023, 2, 1, tzinfo=timezone.utc) + + table = get_fleets_table( + [oldest, newest, middle], current_project="test-project", verbose=False + ) + cells = get_table_cells(table) + + assert [c["NAME"] for c in cells] == ["newest", "middle", "oldest"] def test_empty_fleet(self): fleet = create_backend_fleet(name="empty-fleet", instances=[]) @@ -483,7 +579,7 @@ def test_fleet_with_max_price(self): table = get_fleets_table([fleet], current_project="test-project", verbose=False) cells = get_table_cells(table) - assert cells[0]["PRICE"] == "$0..$5" + assert cells[0]["PRICE"] == "$0..5" def test_fleet_with_multiple_backends(self): fleet = create_backend_fleet( diff --git a/src/tests/_internal/server/routers/test_fleets.py b/src/tests/_internal/server/routers/test_fleets.py index c8f5507796..a6cb5dc7c0 100644 --- a/src/tests/_internal/server/routers/test_fleets.py +++ b/src/tests/_internal/server/routers/test_fleets.py @@ -399,6 +399,30 @@ async def test_lists_fleets(self, test_db, session: AsyncSession, client: AsyncC } ] + @pytest.mark.asyncio + @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) + async def test_lists_fleets_newest_first( + self, test_db, session: AsyncSession, client: AsyncClient + ): + user = await create_user(session, global_role=GlobalRole.USER) + project = await create_project(session) + await add_project_member( + session=session, project=project, user=user, project_role=ProjectRole.USER + ) + for name, day in [("oldest", 1), ("newest", 3), ("middle", 2)]: + await create_fleet( + session=session, + project=project, + name=name, + created_at=datetime(2023, 1, day, tzinfo=timezone.utc), + ) + response = await client.post( + f"/api/project/{project.name}/fleets/list", + headers=get_auth_headers(user.token), + ) + assert response.status_code == 200 + assert [f["name"] for f in response.json()] == ["newest", "middle", "oldest"] + @pytest.mark.asyncio @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) async def test_returns_imported_fleet_with_include_imported(