Add publisher dashboard routes
All checks were successful
buildbot/nix-eval Build done.
buildbot/nix-build Build done.
buildbot/nix-effects Build done.

This commit is contained in:
Abel Luck 2026-06-02 10:18:59 +02:00
parent 96551c2788
commit e4a5246ab3
31 changed files with 1603 additions and 516 deletions

View file

@ -39,6 +39,28 @@ def test_parse_args_supports_dev_mode_flag() -> None:
assert args.dev_mode is True
def test_parse_args_supports_reload_flag() -> None:
command, args = parse_args(["serve", "--reload"])
assert command == "serve"
assert args.reload is True
def test_parse_args_uses_reader_app_url_env_var(monkeypatch) -> None:
monkeypatch.setenv(
"REPUBLISHER_READER_APP_URL",
"https://s3.amazonaws.com/anynews/marti-noticias/index.html",
)
command, args = parse_args(["serve"])
assert command == "serve"
assert (
args.reader_app_url
== "https://s3.amazonaws.com/anynews/marti-noticias/index.html"
)
def test_parse_args_supports_cleanup_media_defaults() -> None:
command, args = parse_args(["cleanup-media"])
@ -169,8 +191,9 @@ def test_entrypoint_passes_dev_mode_to_create_app(monkeypatch) -> None:
def __init__(self) -> None:
self.extensions: dict[str, object] = {}
def fake_create_app(*, dev_mode: bool) -> StubApp:
def fake_create_app(*, dev_mode: bool, reader_app_url: str | None) -> StubApp:
recorded["dev_mode"] = dev_mode
recorded["reader_app_url"] = reader_app_url
return StubApp()
def fake_install_signal_handlers(stop_event: object) -> None:
@ -185,6 +208,7 @@ def test_entrypoint_passes_dev_mode_to_create_app(monkeypatch) -> None:
recorded["app"] = app
recorded["host"] = config.bind[0].split(":")[0]
recorded["port"] = int(config.bind[0].split(":")[1])
recorded["reload"] = config.use_reloader
recorded["shutdown_trigger"] = shutdown_trigger
shutdown_event = cast(Any, app.extensions["repub.shutdown_event"])
recorded["app_shutdown_event"] = shutdown_event
@ -198,12 +222,24 @@ def test_entrypoint_passes_dev_mode_to_create_app(monkeypatch) -> None:
monkeypatch.setattr("repub.entrypoint.hypercorn_serve", fake_hypercorn_serve)
exit_code = entrypoint(
["serve", "--dev-mode", "--host", "0.0.0.0", "--port", "9090"]
[
"serve",
"--dev-mode",
"--reload",
"--host",
"0.0.0.0",
"--port",
"9090",
"--reader-app-url",
"https://reader.example/index.html",
]
)
assert exit_code == 0
assert recorded["dev_mode"] is True
assert recorded["reader_app_url"] == "https://reader.example/index.html"
assert recorded["host"] == "0.0.0.0"
assert recorded["port"] == 9090
assert recorded["reload"] is True
assert recorded["stop_event"] is recorded["app_shutdown_event"]
assert callable(recorded["shutdown_trigger"])

View file

@ -29,10 +29,10 @@ def _configure_trusted_auth(monkeypatch, tmp_path: Path, name: str) -> None:
monkeypatch.setenv("REPUBLISHER_DB_PATH", str(tmp_path / f"{name}.db"))
def _assert_datastar_shell(body: str) -> None:
def _assert_datastar_shell(body: str, *, static_prefix: str) -> None:
assert body.startswith("<!doctype html>")
assert 'id="js"' in body
assert 'src="/static/datastar@1.0.0-RC.8.js"' in body
assert f'src="{static_prefix}/static/datastar@1.0.0-RC.8.js"' in body
assert 'data-init="@post(window.location.pathname +' in body
assert '<main id="morph"' in body
assert "Connecting" in body
@ -62,7 +62,7 @@ def test_trusted_header_mode_rejects_admin_route_without_identity(
async def run() -> None:
client = create_app().test_client()
response = await client.get("/")
response = await client.get("/admin")
assert response.status_code == 401
@ -78,7 +78,7 @@ def test_trusted_header_mode_ignores_generic_forwarded_identity_headers(
client = create_app().test_client()
response = await client.get(
"/",
"/admin",
headers={
"X-Forwarded-User": "mallory",
"X-Forwarded-Email": "mallory@example.org",
@ -100,7 +100,7 @@ def test_trusted_header_mode_rejects_malformed_trusted_identity_headers(
client = create_app().test_client()
response = await client.get(
"/",
"/admin",
headers={
"X-Republisher-Auth-Role": "admin",
"X-Republisher-Auth-Provider": "gp",
@ -120,7 +120,7 @@ def test_trusted_header_mode_allows_admin_identity_on_admin_route(
async def run() -> None:
client = create_app().test_client()
response = await client.get("/", headers=_trusted_headers(role="admin"))
response = await client.get("/admin", headers=_trusted_headers(role="admin"))
assert response.status_code == 200
@ -135,7 +135,9 @@ def test_trusted_header_mode_rejects_publisher_identity_on_admin_route(
async def run() -> None:
client = create_app().test_client()
response = await client.get("/", headers=_trusted_headers(role="publisher"))
response = await client.get(
"/admin", headers=_trusted_headers(role="publisher")
)
assert response.status_code == 403
@ -152,7 +154,7 @@ def test_trusted_header_mode_rejects_admin_action_without_identity(
app.config["REPUB_LOG_DIR"] = tmp_path / "logs"
client = app.test_client()
response = await client.post("/actions/completed-executions/clear")
response = await client.post("/admin/actions/completed-executions/clear")
assert response.status_code == 401
@ -170,7 +172,7 @@ def test_trusted_header_mode_rejects_publisher_identity_on_admin_action(
client = app.test_client()
response = await client.post(
"/actions/completed-executions/clear",
"/admin/actions/completed-executions/clear",
headers=_trusted_headers(role="publisher"),
)
@ -194,12 +196,12 @@ def test_trusted_header_mode_allows_publisher_identity_on_publisher_route(
body = await response.get_data(as_text=True)
assert response.status_code == 200
_assert_datastar_shell(body)
_assert_datastar_shell(body, static_prefix="/publisher")
asyncio.run(run())
def test_trusted_header_mode_publisher_post_serves_hello_publishers_morph(
def test_trusted_header_mode_publisher_post_serves_publisher_dashboard_morph(
monkeypatch, tmp_path: Path
) -> None:
_configure_trusted_auth(monkeypatch, tmp_path, "publisher-post")
@ -220,7 +222,7 @@ def test_trusted_header_mode_publisher_post_serves_hello_publishers_morph(
assert raw_connection.headers["Content-Type"] == "text/event-stream"
assert b"event: datastar-patch-elements" in chunk
assert b'<main id="morph"' in chunk
assert b"Hello publishers" in chunk
assert b"Published feeds" in chunk
await connection.disconnect()
asyncio.run(run())
@ -258,12 +260,12 @@ def test_trusted_header_mode_allows_admin_identity_on_admin_publisher_alias(
body = await response.get_data(as_text=True)
assert response.status_code == 200
_assert_datastar_shell(body)
_assert_datastar_shell(body, static_prefix="/admin")
asyncio.run(run())
def test_trusted_header_mode_admin_publisher_post_serves_hello_publishers_morph(
def test_trusted_header_mode_admin_publisher_post_serves_publisher_dashboard_morph(
monkeypatch, tmp_path: Path
) -> None:
_configure_trusted_auth(monkeypatch, tmp_path, "admin-alias-post")
@ -284,7 +286,7 @@ def test_trusted_header_mode_admin_publisher_post_serves_hello_publishers_morph(
assert raw_connection.headers["Content-Type"] == "text/event-stream"
assert b"event: datastar-patch-elements" in chunk
assert b'<main id="morph"' in chunk
assert b"Hello publishers" in chunk
assert b"Published feeds" in chunk
await connection.disconnect()
asyncio.run(run())
@ -308,7 +310,79 @@ def test_trusted_header_mode_rejects_publisher_identity_on_admin_publisher_alias
asyncio.run(run())
def test_trusted_header_mode_keeps_static_assets_public(
def test_trusted_header_mode_allows_publisher_identity_on_publisher_run_action(
monkeypatch, tmp_path: Path
) -> None:
_configure_trusted_auth(monkeypatch, tmp_path, "publisher-run-action")
async def run() -> None:
client = create_app().test_client()
response = await client.post(
"/publisher/actions/jobs/999/run-now",
headers=_trusted_headers(role="publisher"),
)
assert response.status_code == 204
asyncio.run(run())
def test_trusted_header_mode_rejects_admin_identity_on_publisher_run_action(
monkeypatch, tmp_path: Path
) -> None:
_configure_trusted_auth(monkeypatch, tmp_path, "publisher-run-action-admin")
async def run() -> None:
client = create_app().test_client()
response = await client.post(
"/publisher/actions/jobs/999/run-now",
headers=_trusted_headers(role="admin"),
)
assert response.status_code == 403
asyncio.run(run())
def test_trusted_header_mode_allows_admin_identity_on_admin_publisher_run_action(
monkeypatch, tmp_path: Path
) -> None:
_configure_trusted_auth(monkeypatch, tmp_path, "admin-publisher-run-action")
async def run() -> None:
client = create_app().test_client()
response = await client.post(
"/admin/publisher/actions/jobs/999/run-now",
headers=_trusted_headers(role="admin"),
)
assert response.status_code == 204
asyncio.run(run())
def test_trusted_header_mode_rejects_publisher_identity_on_admin_publisher_run_action(
monkeypatch, tmp_path: Path
) -> None:
_configure_trusted_auth(monkeypatch, tmp_path, "admin-publisher-run-publisher")
async def run() -> None:
client = create_app().test_client()
response = await client.post(
"/admin/publisher/actions/jobs/999/run-now",
headers=_trusted_headers(role="publisher"),
)
assert response.status_code == 403
asyncio.run(run())
def test_trusted_header_mode_keeps_section_static_assets_public(
monkeypatch, tmp_path: Path
) -> None:
_configure_trusted_auth(monkeypatch, tmp_path, "static-public")
@ -316,9 +390,17 @@ def test_trusted_header_mode_keeps_static_assets_public(
async def run() -> None:
client = create_app().test_client()
response = await client.get("/static/datastar@1.0.0-RC.8.js")
for path in (
"/admin/static/datastar@1.0.0-RC.8.js",
"/publisher/static/datastar@1.0.0-RC.8.js",
):
response = await client.get(path)
assert response.status_code == 200
assert response.status_code == 200
root_response = await client.get("/static/datastar@1.0.0-RC.8.js")
assert root_response.status_code == 404
asyncio.run(run())

View file

@ -2,8 +2,9 @@ from __future__ import annotations
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import cast
from repub.jobs import load_runs_view
from repub.jobs import load_dashboard_view, load_runs_view
from repub.model import (
Job,
JobExecution,
@ -232,6 +233,90 @@ def test_load_runs_view_projects_queued_executions_in_fifo_order(
assert view["queued"][1]["move_down_disabled"] is True
def test_load_runs_view_projects_admin_prefixed_action_and_log_paths(
tmp_path: Path,
) -> None:
initialize_database(tmp_path / "jobs-admin-path-prefix.db")
source = create_source(
name="Admin prefixed source",
slug="admin-prefixed-source",
source_type="feed",
notes="",
spider_arguments="",
enabled=True,
cron_minute="*/5",
cron_hour="*",
cron_day_of_month="*",
cron_day_of_week="*",
cron_month="*",
feed_url="https://example.com/admin-prefixed.xml",
)
with database.writer():
job = Job.get(Job.source == source)
running = JobExecution.create(
job=job,
running_status=JobExecutionStatus.RUNNING,
started_at=datetime(2026, 3, 30, 12, 0, tzinfo=UTC),
)
view = load_runs_view(
log_dir=tmp_path / "out" / "logs",
now=datetime(2026, 3, 30, 12, 30, tzinfo=UTC),
path_prefix="/admin",
)
assert (
view["running"][0]["log_href"]
== f"/admin/job/{job.id}/execution/{int(running.get_id())}/logs"
)
assert (
view["running"][0]["cancel_post_path"]
== f"/admin/actions/executions/{int(running.get_id())}/cancel"
)
assert view["upcoming"][0]["run_post_path"] == (
f"/admin/actions/jobs/{job.id}/run-now"
)
assert view["upcoming"][0]["toggle_post_path"] == (
f"/admin/actions/jobs/{job.id}/toggle-enabled"
)
assert view["upcoming"][0]["delete_post_path"] == (
f"/admin/actions/jobs/{job.id}/delete"
)
def test_load_dashboard_view_projects_publisher_run_action_but_root_feed_links(
tmp_path: Path,
) -> None:
initialize_database(tmp_path / "jobs-publisher-dashboard-prefix.db")
source = create_source(
name="Publisher prefixed source",
slug="publisher-prefixed-source",
source_type="feed",
notes="",
spider_arguments="",
enabled=True,
cron_minute="*/5",
cron_hour="*",
cron_day_of_month="*",
cron_day_of_week="*",
cron_month="*",
feed_url="https://example.com/publisher-prefixed.xml",
)
with database.reader():
job = Job.get(Job.source == source)
view = load_dashboard_view(
log_dir=tmp_path / "out" / "logs",
now=datetime(2026, 3, 30, 12, 30, tzinfo=UTC),
path_prefix="/publisher",
)
source_feeds = cast(tuple[dict[str, object], ...], view["source_feeds"])
source_feed = source_feeds[0]
assert source_feed["feed_href"] == "/feeds/publisher-prefixed-source/feed.rss"
assert source_feed["run_post_path"] == (f"/publisher/actions/jobs/{job.id}/run-now")
def test_load_runs_view_keeps_queued_jobs_in_scheduled_jobs(
tmp_path: Path,
) -> None:

View file

@ -1048,7 +1048,7 @@ def test_render_runs_uses_database_backed_jobs_and_executions(
assert "Running jobs" in body
assert "Scheduled jobs" in body
assert "Completed job executions" in body
assert f"/job/{job.id}/execution/{execution.get_id()}/logs" in body
assert f"/admin/job/{job.id}/execution/{execution.get_id()}/logs" in body
assert "Succeeded" in body
assert "Run now" in body
@ -1138,7 +1138,7 @@ def test_delete_job_action_removes_source_job_and_execution_history(
running_status=JobExecutionStatus.SUCCEEDED,
)
response = await client.post(f"/actions/jobs/{job.id}/delete")
response = await client.post(f"/admin/actions/jobs/{job.id}/delete")
assert response.status_code == 204
assert (
@ -1185,7 +1185,7 @@ def test_delete_source_action_removes_source_job_and_execution_history(
running_status=JobExecutionStatus.SUCCEEDED,
)
response = await client.post("/actions/sources/delete-source-row/delete")
response = await client.post("/admin/actions/sources/delete-source-row/delete")
assert response.status_code == 204
assert (

View file

@ -37,6 +37,7 @@ from repub.web import (
render_dashboard,
render_edit_source,
render_execution_logs,
render_publisher,
render_runs,
render_settings,
render_sources,
@ -55,7 +56,12 @@ def _db_writer(fn):
def test_web_routes_do_not_access_peewee_models_directly() -> None:
web_source = Path("repub/web.py").read_text(encoding="utf-8")
web_paths = (
tuple(sorted(Path("repub/web").rglob("*.py")))
if Path("repub/web").is_dir()
else (Path("repub/web.py"),)
)
web_source = "\n".join(path.read_text(encoding="utf-8") for path in web_paths)
assert (
re.search(
@ -111,7 +117,7 @@ def test_action_button_omits_post_handler_when_disabled() -> None:
action_button(
label="Queued",
disabled=True,
post_path="/actions/jobs/7/run-now",
post_path="/admin/actions/jobs/7/run-now",
)
)
@ -138,11 +144,13 @@ def test_action_button_supports_datastar_pointerdown_post() -> None:
action_button(
label="Delete",
tone="danger",
post_path="/actions/jobs/7/delete",
post_path="/admin/actions/jobs/7/delete",
)
)
assert 'data-on:pointerdown="@post(&#39;/actions/jobs/7/delete&#39;)"' in markup
assert (
'data-on:pointerdown="@post(&#39;/admin/actions/jobs/7/delete&#39;)"' in markup
)
def test_runs_page_renders_completed_execution_end_time_as_relative_hoverable_time() -> (
@ -163,7 +171,7 @@ def test_runs_page_renders_completed_execution_end_time_as_relative_hoverable_ti
"status_tone": "done",
"stats": "1 requests • 1 items • 1 bytes",
"summary": "Worker exited successfully",
"log_href": "/job/7/execution/42/logs",
"log_href": "/admin/job/7/execution/42/logs",
},
)
)
@ -195,7 +203,7 @@ def test_runs_page_renders_completed_execution_state_cell_with_duration_and_end_
"status_tone": "done",
"stats": "1 requests • 1 items • 1 bytes",
"summary": "Worker exited successfully",
"log_href": "/job/7/execution/42/logs",
"log_href": "/admin/job/7/execution/42/logs",
},
)
)
@ -234,8 +242,8 @@ def test_runs_page_renders_combined_running_jobs_table() -> None:
"status_tone": "idle",
"run_label": "Queued",
"run_disabled": True,
"run_post_path": "/actions/jobs/7/run-now",
"cancel_post_path": "/actions/queued-executions/42/cancel",
"run_post_path": "/admin/actions/jobs/7/run-now",
"cancel_post_path": "/admin/actions/queued-executions/42/cancel",
"move_up_disabled": True,
"move_up_post_path": None,
"move_down_disabled": True,
@ -249,7 +257,7 @@ def test_runs_page_renders_combined_running_jobs_table() -> None:
assert "queued-source" in body
assert ">Queued<" in body
assert "bg-amber-200 text-amber-950" in body
assert "/actions/queued-executions/42/cancel" in body
assert "/admin/actions/queued-executions/42/cancel" in body
def test_runs_page_renders_running_state_cell_with_duration_and_started_at() -> None:
@ -269,9 +277,9 @@ def test_runs_page_renders_running_state_cell_with_duration_and_started_at() ->
"status": "Running",
"stats": "1 requests • 1 items • 1 byte",
"worker": "streaming stats from worker",
"log_href": "/job/1/execution/11/logs",
"log_href": "/admin/job/1/execution/11/logs",
"cancel_label": "Stop",
"cancel_post_path": "/actions/executions/11/cancel",
"cancel_post_path": "/admin/actions/executions/11/cancel",
},
)
)
@ -316,9 +324,9 @@ def test_runs_page_moves_scheduled_jobs_state_column_to_second_position() -> Non
"run_disabled": False,
"run_reason": "Ready",
"toggle_label": "Disable",
"toggle_post_path": "/actions/jobs/7/toggle-enabled",
"run_post_path": "/actions/jobs/7/run-now",
"delete_post_path": "/actions/jobs/7/delete",
"toggle_post_path": "/admin/actions/jobs/7/toggle-enabled",
"run_post_path": "/admin/actions/jobs/7/run-now",
"delete_post_path": "/admin/actions/jobs/7/delete",
},
)
)
@ -371,7 +379,7 @@ def test_runs_page_renders_clear_completed_button_and_pagination() -> None:
"status_tone": "done",
"stats": "1 requests • 1 items • 1 bytes",
"summary": "Worker exited successfully",
"log_href": f"/job/7/execution/{index}/logs",
"log_href": f"/admin/job/7/execution/{index}/logs",
}
for index in range(1, 21)
)
@ -385,29 +393,41 @@ def test_runs_page_renders_clear_completed_button_and_pagination() -> None:
)
)
assert "/actions/completed-executions/clear" in body
assert "/admin/actions/completed-executions/clear" in body
assert ">Clear history<" in body
assert "Showing" in body
assert "21" in body
assert "@post(&#39;/actions/runs/completed-page/1&#39;)" in body
assert "@post(&#39;/actions/runs/completed-page/2&#39;)" in body
assert "@post(&#39;/admin/actions/runs/completed-page/1&#39;)" in body
assert "@post(&#39;/admin/actions/runs/completed-page/2&#39;)" in body
assert 'aria-current="page"' in body
def test_root_get_serves_datastar_shim() -> None:
def test_root_get_redirects_to_publisher() -> None:
async def run() -> None:
client = create_app().test_client()
response = await client.get("/")
assert response.status_code == 302
assert response.headers["Location"] == "/publisher"
asyncio.run(run())
def test_admin_get_serves_datastar_shim_with_admin_static_assets() -> None:
async def run() -> None:
client = create_app().test_client()
response = await client.get("/admin")
body = await response.get_data(as_text=True)
stylesheet_href = versioned_static_asset_href("app.css")
stylesheet_href = versioned_static_asset_href("app.css", prefix="/admin")
assert response.status_code == 200
assert response.headers["ETag"]
assert body.startswith("<!doctype html>")
assert f'<link rel="stylesheet" href="{stylesheet_href}">' in body
assert (
'<script id="js" defer type="module" src="/static/datastar@1.0.0-RC.8.js"></script>'
'<script id="js" defer type="module" src="/admin/static/datastar@1.0.0-RC.8.js"></script>'
in body
)
assert 'data-signals:tabid="self.crypto.randomUUID().substring(0,8)"' in body
@ -417,47 +437,110 @@ def test_root_get_serves_datastar_shim() -> None:
assert '<main id="morph"' in body
assert "lg:grid-cols-[14rem_minmax(0,1fr)]" in body
assert "lg:px-5 lg:py-4" in body
assert 'href="/sources"' in body
assert 'href="/runs"' in body
assert 'href="/settings"' in body
assert 'href="/admin/sources"' in body
assert 'href="/admin/runs"' in body
assert 'href="/admin/settings"' in body
assert "Connecting" in body
asyncio.run(run())
def test_publisher_get_serves_datastar_shim_with_publisher_static_assets() -> None:
async def run() -> None:
client = create_app().test_client()
response = await client.get("/publisher")
body = await response.get_data(as_text=True)
stylesheet_href = versioned_static_asset_href("app.css", prefix="/publisher")
assert response.status_code == 200
assert f'<link rel="stylesheet" href="{stylesheet_href}">' in body
assert (
'<script id="js" defer type="module" src="/publisher/static/datastar@1.0.0-RC.8.js"></script>'
in body
)
assert '<main id="morph"' in body
assert "Connecting" in body
asyncio.run(run())
def test_old_root_level_admin_routes_do_not_serve_admin_pages() -> None:
async def run() -> None:
client = create_app().test_client()
for path in (
"/sources",
"/sources/create",
"/runs",
"/settings",
"/job/1/execution/1/logs",
):
response = await client.get(path)
assert response.status_code == 404
asyncio.run(run())
def test_versioned_static_asset_href_uses_truncated_file_hash() -> None:
href = versioned_static_asset_href("app.css")
assert re.fullmatch(r"/static/app-[0-9a-f]{12}\.css", href)
assert re.fullmatch(r"/admin/static/app-[0-9a-f]{12}\.css", href)
def test_versioned_static_asset_route_serves_registered_css_file() -> None:
def test_versioned_static_asset_href_supports_publisher_prefix() -> None:
href = versioned_static_asset_href("app.css", prefix="/publisher")
assert re.fullmatch(r"/publisher/static/app-[0-9a-f]{12}\.css", href)
def test_section_static_asset_routes_serve_registered_css_file() -> None:
async def run() -> None:
client = create_app().test_client()
expected = (
Path(__file__).resolve().parents[1] / "repub" / "static" / "app.css"
).read_text(encoding="utf-8")
response = await client.get("/static/app-deadbeefcafe.css")
body = await response.get_data(as_text=True)
for path in (
"/admin/static/app-deadbeefcafe.css",
"/publisher/static/app-deadbeefcafe.css",
):
response = await client.get(path)
body = await response.get_data(as_text=True)
assert response.status_code == 200
assert response.mimetype == "text/css"
assert body == expected
assert response.status_code == 200
assert response.mimetype == "text/css"
assert body == expected
asyncio.run(run())
def test_versioned_static_asset_route_preserves_existing_hyphenated_files() -> None:
def test_section_static_asset_routes_preserve_existing_hyphenated_files() -> None:
async def run() -> None:
client = create_app().test_client()
for path in (
"/admin/static/datastar@1.0.0-RC.8.js",
"/publisher/static/datastar@1.0.0-RC.8.js",
):
response = await client.get(path)
body = await response.get_data(as_text=True)
assert response.status_code == 200
assert response.mimetype == "text/javascript"
assert body.startswith("// Datastar v1.0.0-RC.8")
asyncio.run(run())
def test_root_static_asset_route_no_longer_serves_app_assets() -> None:
async def run() -> None:
client = create_app().test_client()
response = await client.get("/static/datastar@1.0.0-RC.8.js")
body = await response.get_data(as_text=True)
assert response.status_code == 200
assert response.mimetype == "text/javascript"
assert body.startswith("// Datastar v1.0.0-RC.8")
assert response.status_code == 404
asyncio.run(run())
@ -473,14 +556,14 @@ def test_create_app_bootstraps_default_database_path(
assert (tmp_path / "republisher.db").exists()
def test_root_get_honors_if_none_match() -> None:
def test_admin_get_honors_if_none_match() -> None:
async def run() -> None:
client = create_app().test_client()
initial = await client.get("/")
initial = await client.get("/admin")
etag = initial.headers["ETag"]
response = await client.get("/", headers={"If-None-Match": etag})
response = await client.get("/admin", headers={"If-None-Match": etag})
assert response.status_code == 304
assert response.headers["ETag"] == etag
@ -491,7 +574,7 @@ def test_root_get_honors_if_none_match() -> None:
def test_dashboard_post_serves_morph_component() -> None:
async def run() -> None:
client = create_app().test_client()
async with client.request("/?u=shim", method="POST") as connection:
async with client.request("/admin?u=shim", method="POST") as connection:
await connection.send_complete()
chunk = await asyncio.wait_for(connection.receive(), timeout=1)
raw_connection = cast(Any, connection)
@ -651,15 +734,46 @@ def test_render_dashboard_shows_dashboard_information_architecture(
assert "Operational snapshot" in body
assert "Running jobs" in body
assert "Published feeds" in body
assert 'href="/sources"' in body
assert 'href="/runs"' in body
assert "Create source" in body
assert 'href="/admin/sources"' in body
assert 'href="/admin/runs"' in body
assert 'href="/admin/publisher"' in body
assert "Publisher View" in body
assert "Create source" not in body
assert "View sources" not in body
assert "lg:grid-cols-[14rem_minmax(0,1fr)]" in body
assert "lg:px-5 lg:py-4" in body
asyncio.run(run())
def test_render_dashboard_shows_configured_reader_app_link(
monkeypatch, tmp_path: Path
) -> None:
db_path = tmp_path / "dashboard-reader-link.db"
monkeypatch.setenv("REPUBLISHER_DB_PATH", str(db_path))
monkeypatch.setenv(
"REPUBLISHER_READER_APP_URL",
"https://s3.amazonaws.com/anynews/marti-noticias/index.html",
)
async def run() -> None:
app = create_app()
body = str(await render_dashboard(app))
link_match = re.search(
r'<a href="https://s3\.amazonaws\.com/anynews/marti-noticias/index\.html"[^>]*>Open AnyNews</a>',
body,
)
assert link_match is not None
link = link_match.group(0)
assert 'target="_blank"' in link
assert 'rel="noopener noreferrer"' in link
assert "bg-white" in link
assert "bg-amber-400" not in link
asyncio.run(run())
def test_render_dashboard_shows_empty_state_rows(monkeypatch, tmp_path: Path) -> None:
db_path = tmp_path / "dashboard-empty.db"
monkeypatch.setenv("REPUBLISHER_DB_PATH", str(db_path))
@ -688,9 +802,9 @@ def test_dashboard_running_table_matches_runs_page_live_table_markup() -> None:
"status": "Running",
"stats": "1 requests • 1 items • 1 byte",
"worker": "streaming stats from worker",
"log_href": "/job/1/execution/11/logs",
"log_href": "/admin/job/1/execution/11/logs",
"cancel_label": "Stop",
"cancel_post_path": "/actions/executions/11/cancel",
"cancel_post_path": "/admin/actions/executions/11/cancel",
},
)
queued_executions = (
@ -706,8 +820,8 @@ def test_dashboard_running_table_matches_runs_page_live_table_markup() -> None:
"status_tone": "idle",
"run_label": "Queued",
"run_disabled": True,
"run_post_path": "/actions/jobs/2/run-now",
"cancel_post_path": "/actions/queued-executions/22/cancel",
"run_post_path": "/admin/actions/jobs/2/run-now",
"cancel_post_path": "/admin/actions/queued-executions/22/cancel",
"move_up_disabled": True,
"move_up_post_path": None,
"move_down_disabled": True,
@ -733,7 +847,7 @@ def test_dashboard_running_table_matches_runs_page_live_table_markup() -> None:
assert "running-source" in dashboard_body
assert "queued-source" in dashboard_body
assert "bg-sky-100 text-sky-800" in dashboard_body
assert "/job/1/execution/11/logs" in dashboard_body
assert "/admin/job/1/execution/11/logs" in dashboard_body
assert runs_body.count(">State<") >= 1
@ -829,7 +943,11 @@ def test_load_dashboard_view_lists_source_feed_artifacts(
source_feeds = cast(
tuple[dict[str, object], ...],
load_dashboard_view(log_dir=log_dir, now=reference_time)["source_feeds"],
load_dashboard_view(
log_dir=log_dir,
now=reference_time,
path_prefix="/admin",
)["source_feeds"],
)
assert source_feeds == (
@ -845,7 +963,7 @@ def test_load_dashboard_view_lists_source_feed_artifacts(
"next_run": "Not scheduled",
"next_run_at": None,
"run_disabled": False,
"run_post_path": f"/actions/jobs/{available_job.id}/run-now",
"run_post_path": f"/admin/actions/jobs/{available_job.id}/run-now",
"artifact_footprint": "3.0 KB",
},
{
@ -860,7 +978,7 @@ def test_load_dashboard_view_lists_source_feed_artifacts(
"next_run": "Not scheduled",
"next_run_at": None,
"run_disabled": False,
"run_post_path": f"/actions/jobs/{missing_job.id}/run-now",
"run_post_path": f"/admin/actions/jobs/{missing_job.id}/run-now",
"artifact_footprint": "0 B",
},
)
@ -991,19 +1109,164 @@ def test_render_dashboard_shows_source_feed_links_and_statuses(
assert "Never published" in body
assert "Next run" in body
assert ">Run now<" in body
assert f"/actions/jobs/{published_job.id}/run-now" in body
assert f"/actions/jobs/{missing_job.id}/run-now" in body
assert f"/admin/actions/jobs/{published_job.id}/run-now" in body
assert f"/admin/actions/jobs/{missing_job.id}/run-now" in body
assert "data-next-run-at" in body
asyncio.run(run())
def test_render_publisher_shows_published_feeds_with_publisher_actions(
monkeypatch, tmp_path: Path
) -> None:
db_path = tmp_path / "publisher-render.db"
monkeypatch.setenv("REPUBLISHER_DB_PATH", str(db_path))
app = create_app()
app.config["REPUB_LOG_DIR"] = tmp_path / "out" / "logs"
source = create_source(
name="Publisher source",
slug="publisher-source",
source_type="feed",
notes="",
spider_arguments="",
enabled=True,
cron_minute="*/5",
cron_hour="*",
cron_day_of_month="*",
cron_day_of_week="*",
cron_month="*",
feed_url="https://example.com/publisher.xml",
)
async def run() -> None:
feed_path = tmp_path / "out" / "feeds" / "publisher-source" / "feed.rss"
feed_path.parent.mkdir(parents=True)
feed_path.write_text("<rss/>\n", encoding="utf-8")
job = _db_reader(lambda: Job.get(Job.source == source))
body = str(await render_publisher(app, current_path="/publisher"))
assert body.count("Published feeds") == 1
assert "Publisher source" in body
assert 'href="/feeds/publisher-source/feed.rss"' in body
assert "Available" in body
assert "Next run" in body
assert "Disk usage" not in body
assert f"/publisher/actions/jobs/{job.id}/run-now" in body
assert f"/admin/actions/jobs/{job.id}/run-now" not in body
assert 'href="/admin/sources"' not in body
assert "Create source" not in body
assert "Manage sources" not in body
asyncio.run(run())
def test_render_publisher_shows_live_work_without_admin_controls(
monkeypatch, tmp_path: Path
) -> None:
db_path = tmp_path / "publisher-live-work.db"
monkeypatch.setenv("REPUBLISHER_DB_PATH", str(db_path))
app = create_app()
app.config["REPUB_LOG_DIR"] = tmp_path / "out" / "logs"
running_source = create_source(
name="Publisher running source",
slug="publisher-running-source",
source_type="feed",
notes="",
spider_arguments="",
enabled=True,
cron_minute="*/5",
cron_hour="*",
cron_day_of_month="*",
cron_day_of_week="*",
cron_month="*",
feed_url="https://example.com/running.xml",
)
queued_source = create_source(
name="Publisher queued source",
slug="publisher-queued-source",
source_type="feed",
notes="",
spider_arguments="",
enabled=True,
cron_minute="*/5",
cron_hour="*",
cron_day_of_month="*",
cron_day_of_week="*",
cron_month="*",
feed_url="https://example.com/queued.xml",
)
running_job, running_execution, queued_execution = _db_writer(
lambda: (
Job.get(Job.source == running_source),
JobExecution.create(
job=Job.get(Job.source == running_source),
running_status=JobExecutionStatus.RUNNING,
started_at=datetime(2026, 3, 30, 12, 0, tzinfo=UTC),
),
JobExecution.create(
job=Job.get(Job.source == queued_source),
running_status=JobExecutionStatus.PENDING,
),
)
)
async def run() -> None:
body = str(await render_publisher(app, current_path="/publisher"))
assert body.index("Published feeds") < body.index("Live work")
assert "Running jobs" in body
assert "Publisher running source" in body
assert "Publisher queued source" in body
assert "Queue position #1" in body
assert f"/publisher/job/{running_job.id}/execution/" not in body
assert (
f"/publisher/actions/executions/{int(running_execution.get_id())}/cancel"
not in body
)
assert (
f"/publisher/actions/queued-executions/{int(queued_execution.get_id())}/cancel"
not in body
)
assert "View log" not in body
assert ">Stop<" not in body
assert ">Cancel<" not in body
assert body.count(">Actions<") == 1
asyncio.run(run())
def test_render_publisher_shows_configured_reader_app_link(
monkeypatch, tmp_path: Path
) -> None:
db_path = tmp_path / "publisher-reader-link.db"
monkeypatch.setenv("REPUBLISHER_DB_PATH", str(db_path))
monkeypatch.setenv(
"REPUBLISHER_READER_APP_URL",
"https://s3.amazonaws.com/anynews/marti-noticias/index.html",
)
app = create_app()
async def run() -> None:
body = str(await render_publisher(app, current_path="/publisher"))
assert (
'href="https://s3.amazonaws.com/anynews/marti-noticias/index.html"' in body
)
assert 'target="_blank"' in body
assert "Open AnyNews" in body
asyncio.run(run())
def test_render_sources_shows_table_and_create_link() -> None:
async def run() -> None:
body = str(await render_sources())
assert ">Sources<" in body
assert 'href="/sources/create"' in body
assert 'href="/admin/sources/create"' in body
assert "No sources yet." in body
assert "guardian-feed" not in body
assert "podcast-audio" not in body
@ -1048,12 +1311,12 @@ def test_render_sources_shows_live_sidebar_badges(monkeypatch, tmp_path: Path) -
body = str(await render_sources(app))
assert re.search(
r'href="/sources"[^>]*>.*?<span>Sources</span>\s*<span[^>]*>2</span>',
r'href="/admin/sources"[^>]*>.*?<span>Sources</span>\s*<span[^>]*>2</span>',
body,
re.S,
)
assert re.search(
r'href="/runs"[^>]*>.*?<span>Runs</span>\s*<span[^>]*>0</span>',
r'href="/admin/runs"[^>]*>.*?<span>Runs</span>\s*<span[^>]*>0</span>',
body,
re.S,
)
@ -1086,12 +1349,12 @@ def test_render_dashboard_shows_live_sidebar_badges(
body = str(await render_dashboard(app))
assert re.search(
r'href="/sources"[^>]*>.*?<span>Sources</span>\s*<span[^>]*>1</span>',
r'href="/admin/sources"[^>]*>.*?<span>Sources</span>\s*<span[^>]*>1</span>',
body,
re.S,
)
assert re.search(
r'href="/runs"[^>]*>.*?<span>Runs</span>\s*<span[^>]*>0</span>',
r'href="/admin/runs"[^>]*>.*?<span>Runs</span>\s*<span[^>]*>0</span>',
body,
re.S,
)
@ -1125,7 +1388,7 @@ def test_render_sources_shows_delete_action_for_each_source(
assert "Delete" in body
assert "data-on:pointerdown" in body
assert "/actions/sources/delete-me/delete" in body
assert "/admin/actions/sources/delete-me/delete" in body
asyncio.run(run())
@ -1137,7 +1400,7 @@ def test_render_create_source_shows_dedicated_form_page() -> None:
assert ">Create source<" in body
assert "Source and job setup" in body
assert "data-signals__ifmissing" in body
assert "/actions/sources/create" in body
assert "/admin/actions/sources/create" in body
assert 'data-show="$sourceType === &#39;feed&#39;"' in body
assert 'data-show="$sourceType === &#39;pangea&#39;"' in body
assert "jobEnabled" in body
@ -1206,7 +1469,7 @@ def test_render_edit_source_shows_existing_values(monkeypatch, tmp_path: Path) -
body = str(await render_edit_source("kenya-health"))
assert "Edit source" in body
assert "/actions/sources/kenya-health/edit" in body
assert "/admin/actions/sources/kenya-health/edit" in body
assert "Kenya health desk" in body
assert "kenya-health" in body
assert 'id="source-slug"' in body
@ -1239,7 +1502,7 @@ def test_render_settings_shows_current_max_concurrent_jobs(
body = str(await render_settings(app))
assert ">Settings<" in body
assert "/actions/settings" in body
assert "/admin/actions/settings" in body
assert 'value="3"' in body
assert 'value="https://mirror.example"' in body
assert "Max concurrent jobs" in body
@ -1263,7 +1526,7 @@ def test_create_source_action_creates_pangea_source_and_job_in_database(
client = app.test_client()
response = await client.post(
"/actions/sources/create",
"/admin/actions/sources/create",
headers={"Datastar-Request": "true"},
json={
"sourceName": "Kenya health desk",
@ -1291,7 +1554,7 @@ def test_create_source_action_creates_pangea_source_and_job_in_database(
body = await response.get_data(as_text=True)
assert response.status_code == 200
assert "window.location = '/sources'" in body
assert "window.location = '/admin/sources'" in body
source, pangea, job = _db_reader(
lambda: (
@ -1331,7 +1594,7 @@ def test_create_source_action_creates_feed_source_and_job_in_database(
client = app.test_client()
response = await client.post(
"/actions/sources/create",
"/admin/actions/sources/create",
headers={"Datastar-Request": "true"},
json={
"sourceName": "NASA feed",
@ -1351,7 +1614,7 @@ def test_create_source_action_creates_feed_source_and_job_in_database(
body = await response.get_data(as_text=True)
assert response.status_code == 200
assert "window.location = '/sources'" in body
assert "window.location = '/admin/sources'" in body
source, feed, job = _db_reader(
lambda: (
@ -1409,7 +1672,7 @@ def test_edit_source_action_updates_existing_source_and_job_in_database(
client = app.test_client()
response = await client.post(
"/actions/sources/kenya-health/edit",
"/admin/actions/sources/kenya-health/edit",
headers={"Datastar-Request": "true"},
json={
"sourceName": "Kenya health desk nightly",
@ -1440,7 +1703,7 @@ def test_edit_source_action_updates_existing_source_and_job_in_database(
body = await response.get_data(as_text=True)
assert response.status_code == 200
assert "window.location = '/sources'" in body
assert "window.location = '/admin/sources'" in body
source, pangea, job = _db_reader(
lambda: (
@ -1505,7 +1768,7 @@ def test_edit_source_action_rejects_slug_changes(monkeypatch, tmp_path: Path) ->
client = app.test_client()
response = await client.post(
"/actions/sources/kenya-health/edit",
"/admin/actions/sources/kenya-health/edit",
headers={"Datastar-Request": "true"},
json={
"sourceName": "Kenya health desk",
@ -1569,7 +1832,7 @@ def test_create_source_action_validates_duplicate_slug_and_pangea_type(
client = app.test_client()
response = await client.post(
"/actions/sources/create",
"/admin/actions/sources/create",
headers={"Datastar-Request": "true"},
json={
"sourceName": "Duplicate guardian",
@ -1619,7 +1882,7 @@ def test_settings_action_updates_max_concurrent_jobs(
client = app.test_client()
response = await client.post(
"/actions/settings",
"/admin/actions/settings",
headers={"Datastar-Request": "true"},
json={
"maxConcurrentJobs": "3",
@ -1629,7 +1892,7 @@ def test_settings_action_updates_max_concurrent_jobs(
body = await response.get_data(as_text=True)
assert response.status_code == 200
assert "window.location = '/settings'" in body
assert "window.location = '/admin/settings'" in body
assert load_max_concurrent_jobs() == 3
assert load_settings_form()["feed_url"] == "https://mirror.example"
assert 'value="3"' in str(await render_settings(app))
@ -1648,7 +1911,7 @@ def test_settings_action_rejects_non_positive_max_concurrent_jobs(
client = app.test_client()
response = await client.post(
"/actions/settings",
"/admin/actions/settings",
headers={"Datastar-Request": "true"},
json={"maxConcurrentJobs": "0", "feedUrl": "https://mirror.example"},
)
@ -1670,7 +1933,7 @@ def test_settings_action_rejects_invalid_feed_url(monkeypatch, tmp_path: Path) -
client = app.test_client()
response = await client.post(
"/actions/settings",
"/admin/actions/settings",
headers={"Datastar-Request": "true"},
json={"maxConcurrentJobs": "2", "feedUrl": "mirror.example"},
)
@ -1722,7 +1985,7 @@ def test_render_runs_shows_running_scheduled_and_completed_tables(
assert "Scheduled jobs" in body
assert "Completed job executions" in body
assert "runs-render-source" in body
assert f"/job/{job.id}/execution/{execution.get_id()}/logs" in body
assert f"/admin/job/{job.id}/execution/{execution.get_id()}/logs" in body
assert "data-next-run-at" in body
assert "in " in body
@ -1797,7 +2060,7 @@ def test_runs_pagination_action_updates_only_the_current_tab(
)
async with client.request(
"/runs?u=shim",
"/admin/runs?u=shim",
method="POST",
headers={
"Datastar-Request": "true",
@ -1805,7 +2068,7 @@ def test_runs_pagination_action_updates_only_the_current_tab(
},
) as first_connection:
async with client.request(
"/runs?u=shim",
"/admin/runs?u=shim",
method="POST",
headers={
"Datastar-Request": "true",
@ -1825,7 +2088,7 @@ def test_runs_pagination_action_updates_only_the_current_tab(
).decode()
assert (
'href="/runs?completed_page=1" aria-current="page"'
'href="/admin/runs?completed_page=1" aria-current="page"'
not in first_body
)
assert (
@ -1840,7 +2103,7 @@ def test_runs_pagination_action_updates_only_the_current_tab(
) in second_body
response = await client.post(
"/actions/runs/completed-page/2",
"/admin/actions/runs/completed-page/2",
headers={"Datastar-Request": "true"},
json={"tabid": "tab-1"},
)
@ -1878,7 +2141,7 @@ def test_runs_patch_creates_and_cleans_up_tab_state(
client = app.test_client()
async with client.request(
"/runs?u=shim",
"/admin/runs?u=shim",
method="POST",
headers={
"Datastar-Request": "true",
@ -1955,7 +2218,7 @@ def test_render_runs_keeps_queued_execution_in_scheduled_jobs_table(
assert "scheduled-source" in body
assert ">Queued<" in body
assert (
f"/actions/queued-executions/{int(queued_execution.get_id())}/cancel"
f"/admin/actions/queued-executions/{int(queued_execution.get_id())}/cancel"
in body
)
assert "Ready" in body
@ -2004,9 +2267,12 @@ def test_render_runs_shows_cancel_button_for_running_row_with_queued_follow_up(
async def run() -> None:
body = str(await render_runs(app))
assert f"/job/{job.id}/execution/{int(running_execution.get_id())}/logs" in body
assert (
f"/actions/queued-executions/{int(pending_execution.get_id())}/cancel"
f"/admin/job/{job.id}/execution/{int(running_execution.get_id())}/logs"
in body
)
assert (
f"/admin/actions/queued-executions/{int(pending_execution.get_id())}/cancel"
in body
)
assert ">Cancel<" in body
@ -2031,9 +2297,9 @@ def test_render_runs_keeps_all_action_controls_visible_in_html_after_compaction(
"status": "Running",
"stats": "1 requests • 1 items • 1 byte",
"worker": "streaming stats from worker",
"log_href": "/job/1/execution/11/logs",
"log_href": "/admin/job/1/execution/11/logs",
"cancel_label": "Stop",
"cancel_post_path": "/actions/executions/11/cancel",
"cancel_post_path": "/admin/actions/executions/11/cancel",
},
),
queued_executions=(
@ -2049,8 +2315,8 @@ def test_render_runs_keeps_all_action_controls_visible_in_html_after_compaction(
"status_tone": "idle",
"run_label": "Queued",
"run_disabled": True,
"run_post_path": "/actions/jobs/2/run-now",
"cancel_post_path": "/actions/queued-executions/22/cancel",
"run_post_path": "/admin/actions/jobs/2/run-now",
"cancel_post_path": "/admin/actions/queued-executions/22/cancel",
"move_up_disabled": True,
"move_up_post_path": None,
"move_down_disabled": True,
@ -2070,9 +2336,9 @@ def test_render_runs_keeps_all_action_controls_visible_in_html_after_compaction(
"run_disabled": False,
"run_reason": "Ready",
"toggle_label": "Disable",
"toggle_post_path": "/actions/jobs/3/toggle-enabled",
"run_post_path": "/actions/jobs/3/run-now",
"delete_post_path": "/actions/jobs/3/delete",
"toggle_post_path": "/admin/actions/jobs/3/toggle-enabled",
"run_post_path": "/admin/actions/jobs/3/run-now",
"delete_post_path": "/admin/actions/jobs/3/delete",
},
),
completed_executions=(
@ -2087,7 +2353,7 @@ def test_render_runs_keeps_all_action_controls_visible_in_html_after_compaction(
"status_tone": "done",
"stats": "1 requests • 1 items • 1 byte",
"summary": "Worker exited successfully",
"log_href": "/job/4/execution/44/logs",
"log_href": "/admin/job/4/execution/44/logs",
},
),
)
@ -2098,7 +2364,7 @@ def test_render_runs_keeps_all_action_controls_visible_in_html_after_compaction(
assert ">Cancel<" in body
assert ">Run now<" in body
assert ">Disable<" in body
assert "/job/4/execution/44/logs" in body
assert "/admin/job/4/execution/44/logs" in body
def test_cancel_queued_execution_action_deletes_pending_row_without_touching_running_execution(
@ -2143,7 +2409,7 @@ def test_cancel_queued_execution_action_deletes_pending_row_without_touching_run
)
response = await client.post(
f"/actions/queued-executions/{int(pending_execution.get_id())}/cancel"
f"/admin/actions/queued-executions/{int(pending_execution.get_id())}/cancel"
)
assert response.status_code == 204
@ -2217,7 +2483,7 @@ def test_clear_completed_executions_action_removes_history_and_log_artifacts(
completed_prefix.with_suffix(suffix).write_text("history", encoding="utf-8")
running_log_path.write_text("running", encoding="utf-8")
response = await client.post("/actions/completed-executions/clear")
response = await client.post("/admin/actions/completed-executions/clear")
assert response.status_code == 204
assert (
@ -2297,18 +2563,18 @@ def test_move_queued_execution_action_reorders_queue(
)
response = await client.post(
f"/actions/queued-executions/{int(second_execution.get_id())}/move-up"
f"/admin/actions/queued-executions/{int(second_execution.get_id())}/move-up"
)
assert response.status_code == 204
body = str(await render_runs(app))
assert body.index("second-queued-source") < body.index("first-queued-source")
assert (
f"/actions/queued-executions/{int(second_execution.get_id())}/move-down"
f"/admin/actions/queued-executions/{int(second_execution.get_id())}/move-down"
in body
)
assert (
f"/actions/queued-executions/{int(first_execution.get_id())}/move-up"
f"/admin/actions/queued-executions/{int(first_execution.get_id())}/move-up"
in body
)
@ -2349,7 +2615,7 @@ def test_toggle_job_enabled_action_removes_queued_execution(
)
)
response = await client.post(f"/actions/jobs/{job.id}/toggle-enabled")
response = await client.post(f"/admin/actions/jobs/{job.id}/toggle-enabled")
assert response.status_code == 204
assert _db_reader(lambda: Job.get_by_id(job.id).enabled) is False
@ -2361,7 +2627,7 @@ def test_toggle_job_enabled_action_removes_queued_execution(
)
body = str(await render_runs(app))
assert (
f"/actions/queued-executions/{int(queued_execution.get_id())}/cancel"
f"/admin/actions/queued-executions/{int(queued_execution.get_id())}/cancel"
not in body
)
assert "Disabled" in body
@ -2439,7 +2705,7 @@ def test_render_execution_logs_uses_app_route(monkeypatch, tmp_path: Path) -> No
)
assert f"Job {job.id} / execution {execution.get_id()}" in body
assert f"/job/{job.id}/execution/{execution.get_id()}/logs" in body
assert f"/admin/job/{job.id}/execution/{execution.get_id()}/logs" in body
assert "waiting for more log lines" in body
asyncio.run(run())