Close the nine open SDK tasks: one engine per directory, a tabbed dashboard, re-pairing, run recovery
Docs / docs (push) Successful in 27s
Playwright Tests / test-playwright (1, 2) (push) Failing after 17s
Playwright Tests / test-playwright (2, 2) (push) Failing after 12s
pre-commit / pre-commit (push) Failing after 1m59s
Test Backend / test-backend (push) Failing after 2m30s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m19s
Docs / docs (push) Successful in 27s
Playwright Tests / test-playwright (1, 2) (push) Failing after 17s
Playwright Tests / test-playwright (2, 2) (push) Failing after 12s
pre-commit / pre-commit (push) Failing after 1m59s
Test Backend / test-backend (push) Failing after 2m30s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m19s
serve: refuse a second engine for one data directory whatever port it was asked for, using the pidfile and a token this directory signed. The check runs before the database is touched and before the credential is written, which is what left every later CLI call pointing at a dead port. The terminal dashboard is three tabs (Overview, Runs, Logs) with the toolbar following the focused pane, the engine's output goes to serve.log rather than down a pipe, and closing the screen stops both reader threads so the prompt comes back. It adopts a running engine on every start, so stop/start and restart work on one it did not start, and a stop waits for the process to be gone before the next start. Enrolment reports itself in the modal. enroll: a new claim code replaces the pairing instead of being refused. The code is redeemed before anything is written, mappings to a portal being left are cleared, and a running engine redials when the stored enrolment changes. runs: an engine re-queues the runs left `queued` by the one before it, and `fluksio retry <id>` / `retry --group <sweep>` submits an interrupted run again with the same inputs and group, recorded through Run.parent_id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U9BoNGq6V9MdRWAte7JBuC
This commit is contained in:
@@ -4,6 +4,8 @@ import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from fluksio.cli import load_or_create_secret_key
|
||||
|
||||
|
||||
@@ -689,6 +691,41 @@ def test_the_dashboard_runs_the_engine_as_a_child_of_itself(monkeypatch) -> None
|
||||
assert opened == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_the_dashboard_is_three_tabs_and_lets_go_of_its_threads(
|
||||
tmp_path, monkeypatch
|
||||
) -> None:
|
||||
"""The tabs, and what `q` has to do before the screen goes.
|
||||
|
||||
Its two readers are worker threads on the event loop's own executor, which
|
||||
is joined while the loop closes — so a reader still blocked on the log file
|
||||
or the websocket holds the terminal after the dashboard has gone, which is
|
||||
what `q` used to do.
|
||||
"""
|
||||
from textual.widgets import TabbedContent
|
||||
|
||||
from fluksio import cli
|
||||
from fluksio.tui.app import ServeApp
|
||||
|
||||
# Nothing is started under this screen: the engine has its own tests.
|
||||
monkeypatch.setattr(ServeApp, "start_engine", lambda self: None)
|
||||
args = cli._parser().parse_args(["serve", "--data-dir", str(tmp_path / ".fluksio")])
|
||||
app = ServeApp(args)
|
||||
async with app.run_test() as pilot:
|
||||
tabs = app.query_one(TabbedContent)
|
||||
assert tabs.active == "overview-tab"
|
||||
await pilot.press("2")
|
||||
assert tabs.active == "runs-tab"
|
||||
# The run keys live on the table, so they are in the footer only here.
|
||||
assert "app.pick" in {binding[1] for binding in app.query_one("#runs").BINDINGS}
|
||||
await pilot.press("3")
|
||||
assert tabs.active == "logs-tab"
|
||||
await pilot.press("q")
|
||||
|
||||
assert app.stop_log.is_set()
|
||||
assert app.stop_stream.is_set()
|
||||
|
||||
|
||||
def test_a_serving_engine_records_itself_until_it_stops(tmp_path) -> None:
|
||||
"""A pid nobody is running is the same as no pidfile at all."""
|
||||
import os
|
||||
@@ -741,6 +778,41 @@ def test_who_holds_the_port_is_told_apart_by_the_token() -> None:
|
||||
assert probe_engine("http://127.0.0.1:1", "t") == "other"
|
||||
|
||||
|
||||
def test_a_second_engine_for_one_directory_is_refused(tmp_path, monkeypatch, capsys):
|
||||
"""One SQLite file, one engine — whatever port the second was asked for.
|
||||
|
||||
The refusal is worth more than the duplicate it prevents: the second
|
||||
engine signs in on the way up, so `client.json` would point at a port
|
||||
that dies with it and every later command would reach nothing.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
from fluksio import cli
|
||||
|
||||
data_dir = tmp_path / ".fluksio"
|
||||
data_dir.mkdir()
|
||||
(data_dir / "client.json").write_text(
|
||||
json.dumps({"url": "http://127.0.0.1:8000", "token": "the-first-one"})
|
||||
)
|
||||
cli.write_pidfile(data_dir, 8000)
|
||||
monkeypatch.setattr(cli, "probe_engine", lambda *a, **k: "ours")
|
||||
|
||||
args = cli._parser().parse_args(
|
||||
["serve", "--plain", "--port", "8123", "--data-dir", str(data_dir)]
|
||||
)
|
||||
assert cli.cmd_serve(args) == 0
|
||||
said = capsys.readouterr().out
|
||||
assert "already serving" in said and str(os.getpid()) in said
|
||||
# The credential still names the engine that is actually up.
|
||||
stored = json.loads((data_dir / "client.json").read_text())
|
||||
assert stored["url"] == "http://127.0.0.1:8000"
|
||||
|
||||
# A pid that is alive but is not an engine of ours is not a refusal.
|
||||
monkeypatch.setattr(cli, "probe_engine", lambda *a, **k: "foreign")
|
||||
assert cli.already_serving(data_dir, "127.0.0.1") == ""
|
||||
|
||||
|
||||
def test_serve_moves_off_a_port_that_is_taken() -> None:
|
||||
"""A first start should not die on somebody else's dev server."""
|
||||
import socket
|
||||
|
||||
Reference in New Issue
Block a user