Update
DevPlace CI / test (push) Failing after 1h28m53s

This commit is contained in:
2026-09-03 08:47:57 +02:00
parent 70ceb3cf81
commit 3d07a478c3
41 changed files with 1643 additions and 130 deletions
+56
View File
@@ -107,3 +107,59 @@ def test_http_ingress_proxy(app_server):
httpd.shutdown()
get_table("instances").delete(uid=uid)
refresh_snapshot()
def test_http_ingress_proxy_injects_a_base_into_the_root_document_only(app_server):
import http.server
import socket
import socketserver
import threading
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.close()
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(
b"<!doctype html><html><head><title>t</title></head>"
b"<body><script src='app.js'></script></body></html>"
)
def log_message(self, *args):
pass
httpd = socketserver.TCPServer(("127.0.0.1", port), Handler)
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
thread.start()
slug = f"base{port}"
uid = f"basetest-{port}"
get_table("instances").insert(
{
"uid": uid,
"name": "ingress-base",
"project_uid": "ingtest",
"status": "running",
"ingress_slug": slug,
"ingress_port": 8000,
"ports_json": f'[{{"host": {port}, "container": 8000, "proto": "tcp"}}]',
"deleted_at": None,
"deleted_by": None,
}
)
refresh_snapshot()
try:
root = requests.get(f"{BASE_URL}/p/{slug}")
assert root.status_code == 200, root.text
assert f'<base href="/p/{slug}/">' in root.text
nested = requests.get(f"{BASE_URL}/p/{slug}/static/webview/pre/index.html")
assert nested.status_code == 200, nested.text
assert "<base" not in nested.text
finally:
httpd.shutdown()
get_table("instances").delete(uid=uid)
refresh_snapshot()
+73
View File
@@ -1082,3 +1082,76 @@ def test_republishing_a_live_tunnel_keeps_its_certificate():
assert again["uid"] == row["uid"]
assert again["status"] == tunnels.STATUS_ACTIVE
assert again["last_error"] == ""
def _editor_listener():
import socket
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
sock.listen(8)
return sock
def _workspace_json(slug: str, key: str) -> dict:
import requests
return requests.get(
f"{BASE_URL}/projects/{slug}/workspace",
headers={"Accept": "application/json", "X-API-KEY": key},
timeout=HTTP_TIMEOUT,
).json()
def test_workspace_json_reports_the_phase_from_a_live_editor_probe(app_server, seeded_db):
owner = _seeded_user("bob_test")
project = _http_project(owner["uid"], "WS Phase Http")
sock = _editor_listener()
port = sock.getsockname()[1]
try:
_instance(
project_uid=project["uid"],
workspace_owner_uid=owner["uid"],
editor_port=editor.EDITOR_DEFAULT_PORT,
ports_json=(
f'[{{"host": {port}, "container": {editor.EDITOR_DEFAULT_PORT}, '
'"proto": "tcp"}]'
),
)
body = _workspace_json(project["slug"], owner["api_key"])
assert body["workspace"]["phase"] == provision.PHASE_READY
assert body["workspace"]["phase_label"] == "Ready"
assert body["workspace"]["editor_ready"] is True
assert body["workspace"]["owner_uid"] == owner["uid"]
assert body["editor_url"].endswith("/code/")
finally:
sock.close()
body = _workspace_json(project["slug"], owner["api_key"])
assert body["workspace"]["phase"] == provision.PHASE_STARTING
assert body["workspace"]["phase_label"] == "Starting"
assert body["workspace"]["editor_ready"] is False
assert body["workspace"]["status"] == "running"
def test_editor_proxy_refuses_a_stopped_workspace_as_plain_text(app_server, seeded_db):
import requests
owner = _seeded_user("bob_test")
project = _http_project(owner["uid"], "WS Proxy Stopped")
instance = _instance(
project_uid=project["uid"],
workspace_owner_uid=owner["uid"],
status="stopped",
desired_state="stopped",
)
response = requests.get(
f"{BASE_URL}/projects/{project['slug']}/containers/instances/{instance['uid']}/code/",
headers={"X-API-KEY": owner["api_key"]},
timeout=HTTP_TIMEOUT,
)
assert response.status_code == 409, response.text[:400]
assert response.headers["content-type"].startswith("text/plain")
assert "not running" in response.text
+119 -5
View File
@@ -1,6 +1,7 @@
# retoor <retoor@molodetz.nl>
import re
import socket
import time
from uuid import uuid4
@@ -14,6 +15,9 @@ from devplacepy.services.containers.workspace import flags, naming
from devplacepy.utils import make_combined_slug
from tests.conftest import BASE_URL
EDITOR_CONTAINER_PORT = 8443
TRANSITION_TIMEOUT_MS = 8000
@pytest.fixture(autouse=True)
def _workspaces_on():
@@ -62,7 +66,20 @@ def _project_for(owner_uid: str, title: str = "WS Project") -> dict:
return row
def _workspace_for(project: dict, owner_uid: str, **overrides) -> dict:
@pytest.fixture(scope="module")
def editor_listener():
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
sock.listen(16)
try:
yield sock.getsockname()[1]
finally:
sock.close()
def _workspace_for(
project: dict, owner_uid: str, editor_port: int = 0, **overrides
) -> dict:
payload = {
"project_uid": project["uid"],
"name": "ws-e2e",
@@ -73,10 +90,20 @@ def _workspace_for(project: dict, owner_uid: str, **overrides) -> dict:
"tunnel_name": naming.generate(),
"ports_json": '[{"host": 20777, "container": 8080, "proto": "tcp"}]',
}
if editor_port:
payload["editor_port"] = EDITOR_CONTAINER_PORT
payload["ports_json"] = (
f'[{{"host": {editor_port}, "container": {EDITOR_CONTAINER_PORT}, '
'"proto": "tcp"}]'
)
payload.update(overrides)
return store.create_instance(payload)
def _phase_view(page, phase: str):
return page.locator(f".workspace-phase-view[data-phase-view~='{phase}']")
def test_project_page_offers_the_workspace_entry_point_to_the_owner(alice):
page, user = alice
project = _project_for(_row_for(user)["uid"], "WS Entry")
@@ -104,11 +131,13 @@ def test_project_page_hides_the_workspace_entry_point_from_a_non_owner(bob):
assert not page.locator(".context-menu-item:has-text('Workspace')").count()
def test_project_page_shows_a_direct_vscode_button_for_a_running_workspace(alice):
def test_project_page_shows_a_direct_vscode_button_for_a_running_workspace(
alice, editor_listener
):
page, user = alice
row = _row_for(user)
project = _project_for(row["uid"], "WS Direct")
instance = _workspace_for(project, row["uid"])
instance = _workspace_for(project, row["uid"], editor_port=editor_listener)
page.goto(
f"{BASE_URL}/projects/{project['slug']}", wait_until="domcontentloaded"
)
@@ -383,11 +412,11 @@ def test_resetting_editor_preferences_restores_the_site_default(alice):
get_table("workspace_editor_prefs").delete(owner_id=row["uid"])
def test_the_editor_launch_control_carries_the_window_profile(alice):
def test_the_editor_launch_control_carries_the_window_profile(alice, editor_listener):
page, user = alice
row = _row_for(user)
project = _project_for(row["uid"], "WS Editor Launch")
instance = _workspace_for(project, row["uid"])
instance = _workspace_for(project, row["uid"], editor_port=editor_listener)
page.goto(
f"{BASE_URL}/projects/{project['slug']}/workspace",
wait_until="domcontentloaded",
@@ -415,3 +444,88 @@ def test_the_workspace_help_states_that_every_folder_is_trusted(alice):
help_card.wait_for(state="visible")
expect(help_card).to_contain_text("Restricted Mode")
expect(help_card).to_contain_text("dpc")
def test_direct_vscode_button_is_absent_while_the_editor_is_unreachable(alice):
page, user = alice
row = _row_for(user)
project = _project_for(row["uid"], "WS Booting")
_workspace_for(project, row["uid"])
page.goto(
f"{BASE_URL}/projects/{project['slug']}", wait_until="domcontentloaded"
)
page.locator(".project-detail-actions").wait_for(state="visible")
assert not page.locator(".project-detail-actions a[data-editor-open]").count()
def test_a_running_workspace_whose_editor_is_not_listening_reads_starting(alice):
page, user = alice
row = _row_for(user)
project = _project_for(row["uid"], "WS Not Listening")
_workspace_for(project, row["uid"])
page.goto(
f"{BASE_URL}/projects/{project['slug']}/workspace",
wait_until="domcontentloaded",
)
expect(page.locator("[data-workspace-phase]")).to_have_text("Starting")
expect(_phase_view(page, "starting")).to_be_visible()
expect(_phase_view(page, "starting").locator(".btn.is-loading")).to_be_disabled()
expect(_phase_view(page, "ready")).to_be_hidden()
assert not page.locator(".workspace-actions a[data-editor-open]").is_visible()
def test_start_shows_progress_and_flips_to_open_editor_when_the_editor_answers(
alice, editor_listener
):
page, user = alice
row = _row_for(user)
project = _project_for(row["uid"], "WS Start Flow")
instance = _workspace_for(
project,
row["uid"],
editor_port=editor_listener,
status="stopped",
desired_state="stopped",
)
page.goto(
f"{BASE_URL}/projects/{project['slug']}/workspace",
wait_until="domcontentloaded",
)
expect(page.locator("[data-workspace-phase]")).to_have_text("Stopped")
start = _phase_view(page, "stopped").locator("button[type='submit']")
expect(start).to_be_visible()
start.click()
expect(_phase_view(page, "starting")).to_be_visible(timeout=TRANSITION_TIMEOUT_MS)
expect(_phase_view(page, "stopped")).to_be_hidden()
expect(page.locator("[data-workspace-phase]")).to_have_text("Starting")
assert store.get_instance(instance["uid"])["desired_state"] == "running"
store.update_instance(instance["uid"], {"status": "running"})
launch = page.locator(".workspace-actions a[data-editor-open]")
launch.wait_for(state="visible", timeout=TRANSITION_TIMEOUT_MS)
expect(page.locator("[data-workspace-phase]")).to_have_text("Ready")
expect(_phase_view(page, "starting")).to_be_hidden()
expect(launch).to_have_attribute(
"href",
f"/projects/{project['slug']}/containers/instances/{instance['uid']}/code/",
)
def test_stop_shows_progress_until_the_container_is_down(alice, editor_listener):
page, user = alice
row = _row_for(user)
project = _project_for(row["uid"], "WS Stop Flow")
instance = _workspace_for(project, row["uid"], editor_port=editor_listener)
page.goto(
f"{BASE_URL}/projects/{project['slug']}/workspace",
wait_until="domcontentloaded",
)
expect(page.locator("[data-workspace-phase]")).to_have_text("Ready")
_phase_view(page, "ready").locator("form[data-workspace-action] button").click()
expect(_phase_view(page, "stopping")).to_be_visible(timeout=TRANSITION_TIMEOUT_MS)
expect(page.locator("[data-workspace-phase]")).to_have_text("Stopping")
assert store.get_instance(instance["uid"])["desired_state"] == "stopped"
store.update_instance(instance["uid"], {"status": "stopped"})
expect(_phase_view(page, "stopped")).to_be_visible(timeout=TRANSITION_TIMEOUT_MS)
expect(page.locator("[data-workspace-phase]")).to_have_text("Stopped")
+45
View File
@@ -0,0 +1,45 @@
# retoor <retoor@molodetz.nl>
from devplacepy.services.containers import forward
PREFIX = "/projects/demo/containers/instances/abc/code"
def test_the_root_document_is_the_empty_or_slash_path():
assert forward.is_root_document("") is True
assert forward.is_root_document("/") is True
assert forward.is_root_document("//") is True
def test_a_nested_path_is_never_the_root_document():
for path in (
"index.html",
"login",
"stable-abc/static/out/vs/workbench/contrib/webview/browser/pre/index.html",
"nested/page.html",
"/nested/",
):
assert forward.is_root_document(path) is False, path
def test_inject_base_adds_a_base_after_head():
body = b"<!doctype html><html><head><meta charset=utf-8></head><body>x</body></html>"
injected = forward.inject_base(body, PREFIX)
assert injected.startswith(b"<!doctype html><html><head>" + f'<base href="{PREFIX}/">'.encode())
def test_inject_base_leaves_an_existing_base_alone():
body = b'<html><head><base href="./"></head><body></body></html>'
assert forward.inject_base(body, PREFIX) == body
def test_inject_base_prepends_when_there_is_no_head_or_html():
body = b"<p>fragment</p>"
assert forward.inject_base(body, PREFIX) == f'<base href="{PREFIX}/">'.encode() + body
def test_rewrite_location_prefixes_only_absolute_paths():
assert forward.rewrite_location("/login", PREFIX) == f"{PREFIX}/login"
assert forward.rewrite_location("./login", PREFIX) == "./login"
assert forward.rewrite_location("//cdn.example/x", PREFIX) == "//cdn.example/x"
assert forward.rewrite_location("/login", "") == "/login"
@@ -0,0 +1,178 @@
# retoor <retoor@molodetz.nl>
import itertools
import socket
import pytest
from devplacepy.database import get_table, init_db
from devplacepy.services.containers import store
from devplacepy.services.containers.workspace import provision
OWNER = "provision-owner"
STATUSES = (
store.ST_CREATED,
store.ST_STARTING,
store.ST_RUNNING,
store.ST_STOPPED,
store.ST_PAUSED,
store.ST_CRASHED,
store.ST_RESTARTING,
store.ST_REMOVING,
store.ST_REMOVED,
"",
)
DESIRED = (store.DESIRED_RUNNING, store.DESIRED_STOPPED, store.DESIRED_PAUSED, "")
@pytest.fixture(autouse=True)
def _provision_db():
init_db()
yield
get_table("instances").delete(workspace_owner_uid=OWNER)
def _instance(**overrides) -> dict:
row = {
"uid": "ws-phase",
"status": store.ST_RUNNING,
"desired_state": store.DESIRED_RUNNING,
"suspended_at": "",
"editor_port": 8443,
"ports_json": "[]",
"container_ip": "",
"workspace_owner_uid": OWNER,
}
row.update(overrides)
return row
@pytest.fixture
def listener():
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
sock.listen(4)
try:
yield sock
finally:
sock.close()
@pytest.mark.parametrize(
"status, desired, ready, expected",
[
(store.ST_RUNNING, store.DESIRED_RUNNING, True, provision.PHASE_READY),
(store.ST_RUNNING, store.DESIRED_RUNNING, False, provision.PHASE_STARTING),
(store.ST_CREATED, store.DESIRED_RUNNING, False, provision.PHASE_STARTING),
(store.ST_STOPPED, store.DESIRED_RUNNING, False, provision.PHASE_STARTING),
(store.ST_CRASHED, store.DESIRED_RUNNING, False, provision.PHASE_STARTING),
(store.ST_RUNNING, store.DESIRED_STOPPED, True, provision.PHASE_STOPPING),
(store.ST_PAUSED, store.DESIRED_STOPPED, False, provision.PHASE_STOPPING),
(store.ST_RESTARTING, store.DESIRED_STOPPED, False, provision.PHASE_STOPPING),
(store.ST_CRASHED, store.DESIRED_STOPPED, False, provision.PHASE_CRASHED),
(store.ST_STOPPED, store.DESIRED_STOPPED, False, provision.PHASE_STOPPED),
(store.ST_CREATED, store.DESIRED_STOPPED, False, provision.PHASE_STOPPED),
("", "", False, provision.PHASE_STOPPED),
],
)
def test_phase_follows_desired_state_status_and_the_editor_probe(
status, desired, ready, expected
):
row = _instance(status=status, desired_state=desired)
assert provision.phase(row, ready) == expected
def test_a_suspended_workspace_is_suspended_whatever_else_it_says():
for status, desired, ready in itertools.product(STATUSES, DESIRED, (True, False)):
row = _instance(
status=status, desired_state=desired, suspended_at="2026-01-01T00:00:00"
)
assert provision.phase(row, ready) == provision.PHASE_SUSPENDED
def test_ready_needs_a_running_container_that_wants_to_run():
for status, desired in itertools.product(STATUSES, DESIRED):
row = _instance(status=status, desired_state=desired)
is_ready = provision.phase(row, True) == provision.PHASE_READY
assert is_ready == (
status == store.ST_RUNNING and desired == store.DESIRED_RUNNING
)
def test_every_phase_over_the_whole_input_domain_has_a_label():
for status, desired, ready, suspended in itertools.product(
STATUSES, DESIRED, (True, False), ("", "2026-01-01T00:00:00")
):
row = _instance(status=status, desired_state=desired, suspended_at=suspended)
assert provision.phase(row, ready) in provision.PHASE_LABELS
def test_transitional_phases_are_exactly_starting_and_stopping():
assert set(provision.TRANSITIONAL_PHASES) == {
provision.PHASE_STARTING,
provision.PHASE_STOPPING,
}
def test_editor_ready_when_the_editor_port_accepts_connections(listener):
port = listener.getsockname()[1]
row = _instance(container_ip="127.0.0.1", editor_port=port)
assert provision.editor_ready(row) is True
def test_editor_ready_is_false_when_nothing_listens():
probe = socket.socket()
probe.bind(("127.0.0.1", 0))
port = probe.getsockname()[1]
probe.close()
row = _instance(container_ip="127.0.0.1", editor_port=port)
assert provision.editor_ready(row) is False
def test_editor_ready_is_false_without_a_reachable_target(listener):
row = _instance(container_ip="", ports_json="[]")
assert provision.editor_ready(row) is False
def test_editor_ready_is_false_unless_the_container_runs(listener):
port = listener.getsockname()[1]
for status in STATUSES:
if status == store.ST_RUNNING:
continue
row = _instance(container_ip="127.0.0.1", editor_port=port, status=status)
assert provision.editor_ready(row) is False
def test_editor_ready_is_false_while_suspended(listener):
port = listener.getsockname()[1]
row = _instance(
container_ip="127.0.0.1", editor_port=port, suspended_at="2026-01-01T00:00:00"
)
assert provision.editor_ready(row) is False
def test_view_carries_the_phase_its_label_readiness_and_the_owner(listener):
port = listener.getsockname()[1]
instance = store.create_instance(
{
"project_uid": "provision-project",
"name": "ws-view",
"status": store.ST_RUNNING,
"desired_state": store.DESIRED_RUNNING,
"is_workspace": 1,
"workspace_owner_uid": OWNER,
"container_ip": "127.0.0.1",
"editor_port": port,
"ports_json": "[]",
}
)
view = provision.view(instance)
assert view["phase"] == provision.PHASE_READY
assert view["phase_label"] == "Ready"
assert view["editor_ready"] is True
assert view["owner_uid"] == OWNER
store.update_instance(instance["uid"], {"desired_state": store.DESIRED_STOPPED})
view = provision.view(store.get_instance(instance["uid"]))
assert view["phase"] == provision.PHASE_STOPPING
assert view["editor_ready"] is True
+92
View File
@@ -0,0 +1,92 @@
# retoor <retoor@molodetz.nl>
import pytest
from devplacepy.database import get_table, init_db
from devplacepy.services import live_view_relay
from devplacepy.services.containers import store
from devplacepy.services.containers.workspace import provision
from tests.conftest import run_async
PROJECT_UID = "relay-ws-project"
OWNER = "relay-ws-owner"
OTHER = "relay-ws-other"
@pytest.fixture(autouse=True)
def _relay_db():
init_db()
get_table("projects").insert(
{
"uid": PROJECT_UID,
"user_uid": OWNER,
"title": "Relay",
"description": "",
"slug": "relay-ws",
"project_type": "software",
"status": "In Development",
"platforms": "",
"is_private": 0,
"read_only": 0,
"stars": 0,
"created_at": "2026-01-01T00:00:00+00:00",
"deleted_at": None,
"deleted_by": None,
}
)
yield
get_table("instances").delete(project_uid=PROJECT_UID)
get_table("projects").delete(uid=PROJECT_UID)
def _handler(topic: str):
for pattern, compute, interval in live_view_relay.VIEWS:
match = pattern.match(topic)
if match is not None:
return compute, match, interval
raise AssertionError(f"no live view for {topic}")
def _workspace(**overrides) -> dict:
row = {
"project_uid": PROJECT_UID,
"name": "ws-relay",
"status": store.ST_RUNNING,
"desired_state": store.DESIRED_RUNNING,
"is_workspace": 1,
"workspace_owner_uid": OWNER,
"ports_json": "[]",
}
row.update(overrides)
return store.create_instance(row)
def test_the_workspace_topic_lives_in_the_owner_namespace():
instance = _workspace()
compute, match, interval = _handler(f"user.{OWNER}.workspace.{instance['uid']}")
assert compute is live_view_relay._workspace_detail
assert interval == 3.0
payload = run_async(compute(match))
assert payload["workspace"]["uid"] == instance["uid"]
assert payload["workspace"]["phase"] == provision.PHASE_STARTING
assert payload["workspace"]["editor_ready"] is False
assert payload["editor_url"] == (
f"/projects/relay-ws/containers/instances/{instance['uid']}/code/"
)
def test_the_workspace_topic_never_answers_another_owner():
instance = _workspace()
compute, match, _interval = _handler(f"user.{OTHER}.workspace.{instance['uid']}")
assert run_async(compute(match)) is None
def test_the_workspace_topic_ignores_a_plain_container():
instance = _workspace(is_workspace=0)
compute, match, _interval = _handler(f"user.{OWNER}.workspace.{instance['uid']}")
assert run_async(compute(match)) is None
def test_the_workspace_topic_ignores_an_unknown_instance():
compute, match, _interval = _handler(f"user.{OWNER}.workspace.does-not-exist")
assert run_async(compute(match)) is None