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

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:
2026-08-31 17:37:13 +02:00
co-authored by Claude Opus 5
parent bdad6d7fc2
commit 8a94bf10d7
14 changed files with 873 additions and 140 deletions
+72
View File
@@ -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