# retoor import json import pytest from devplacepy.content import can_manage_workspace, can_open_workspace from devplacepy.database import get_table, init_db, set_setting from devplacepy.services.containers import activity, api, store from devplacepy.services.containers.workspace import ( flags, naming, provision, quota, tunnels, ) from devplacepy.services.containers.workspace.provision import WorkspaceError from tests.conftest import BASE_URL, run_async OWNER = "user-owner" OTHER = "user-other" # A workspace route that answers wrongly must fail the test, never wedge the serial suite. HTTP_TIMEOUT = 30 @pytest.fixture(autouse=True) def _workspace_db(): init_db() set_setting("workspace_enabled", "1") yield for table in ("instances", "tunnels", "workspace_flags", "workspace_quota_rules"): get_table(table).delete() def _project(uid: str = "proj-ws") -> dict: return {"uid": uid, "slug": "demo", "title": "Demo", "user_uid": OWNER} def _instance(**overrides) -> dict: row = { "project_uid": "proj-ws", "name": "ws-demo", "status": "running", "desired_state": "running", "is_workspace": 1, "workspace_owner_uid": OWNER, "tunnel_name": naming.generate(), "ports_json": '[{"host": 20500, "container": 8080, "proto": "tcp"}]', } row.update(overrides) return store.create_instance(row) def test_open_workspace_requires_enabled_setting(): set_setting("workspace_enabled", "0") assert can_open_workspace(_project(), {"uid": OWNER, "role": "Member"}) is False set_setting("workspace_enabled", "1") assert can_open_workspace(_project(), {"uid": OWNER, "role": "Member"}) is True def test_guest_can_never_open_workspace(): assert can_open_workspace(_project(), None) is False assert can_open_workspace(_project(), {}) is False def test_non_owner_member_cannot_open_workspace(): assert can_open_workspace(_project(), {"uid": OTHER, "role": "Member"}) is False def test_admin_can_open_any_project_workspace(): assert can_open_workspace(_project(), {"uid": OTHER, "role": "Admin"}) is True def test_owner_manages_own_workspace_without_admin(): instance = _instance() assert can_manage_workspace(instance, _project(), {"uid": OWNER, "role": "Member"}) assert not can_manage_workspace( instance, _project(), {"uid": OTHER, "role": "Member"} ) def test_create_or_resume_is_idempotent(): project = _project() user = {"uid": OWNER, "username": "owner"} first = run_async(provision.ensure(project, user)) second = run_async(provision.ensure(project, user)) assert first["uid"] == second["uid"] assert provision.count_for_owner(OWNER) == 1 def test_workspace_quota_blocks_beyond_limit(): set_setting("workspace_max_per_user", "1") user = {"uid": OWNER, "username": "owner"} run_async(provision.ensure(_project("p-a"), user)) with pytest.raises(WorkspaceError): run_async(provision.ensure(_project("p-b"), user)) set_setting("workspace_max_per_user", "2") def test_tunnel_revives_rather_than_duplicates(): instance = _instance() first = tunnels.create(instance, "web", 8080, OWNER) tunnels.soft_delete(first["uid"], OWNER) assert tunnels.count_for_instance(instance["uid"]) == 0 revived = tunnels.create(instance, "web", 8080, OWNER) assert revived["uid"] == first["uid"] assert tunnels.count_for_instance(instance["uid"]) == 1 def test_tunnel_hostname_is_a_single_dns_label(): instance = _instance() row = tunnels.create(instance, "web", 3000, OWNER) host = row["hostname"] suffix = "." + naming.domain() assert host.endswith(suffix) label = host[: -len(suffix)] assert "." not in label assert naming.is_valid_label(label) def test_suspend_stops_tunnels_without_deleting_them(): instance = _instance() tunnels.create(instance, "web", 8080, OWNER) provision.suspend(instance, "admin-uid", "abuse") rows = tunnels.list_for_instance(instance["uid"]) assert rows and rows[0]["status"] == "suspended" refreshed = store.get_instance(instance["uid"]) assert refreshed["suspended_at"] provision.unsuspend(refreshed) assert tunnels.list_for_instance(instance["uid"])[0]["status"] == "pending" def test_suspended_workspace_cannot_resume(): instance = _instance() provision.suspend(instance, "admin-uid", "abuse") with pytest.raises(WorkspaceError): provision.resume(store.get_instance(instance["uid"])) def test_flags_are_idempotent_while_open(): instance = _instance() first = flags.raise_flag(instance, flags.KIND_EGRESS, "warn", "a", 1.0, 0.5) second = flags.raise_flag(instance, flags.KIND_EGRESS, "warn", "b", 2.0, 0.5) assert first["uid"] == second["uid"] assert len(flags.list_flags(instance_uid=instance["uid"])) == 1 flags.clear_flag(instance["uid"], flags.KIND_EGRESS, "admin") assert flags.list_flags(instance_uid=instance["uid"]) == [] def test_activity_accumulates_egress_and_requests(): instance = _instance() activity.forget(instance["uid"]) for _ in range(3): activity.touch(instance["uid"], egress_bytes=100) activity.flush(instance["uid"]) row = store.get_instance(instance["uid"]) assert row["egress_bytes"] == 300 assert row["request_count"] == 3 assert row["last_active_at"] def test_quota_rule_overrides_only_its_owner(): get_table("workspace_quota_rules").insert( { "uid": "rule-x", "owner_kind": "user", "owner_id": OWNER, "label": "power", "max_workspaces": 7, "deleted_at": None, "deleted_by": None, } ) assert quota.resolve(OWNER).max_workspaces == 7 assert quota.resolve(OTHER).max_workspaces == quota.resolve().max_workspaces def test_workspace_env_contract_is_complete(): from devplacepy.services.containers import api instance = _instance() env = api.workspace_env(instance, "https://example.test") for key in ( "DEVPLACE_WORKSPACE", "DEVPLACE_WORKSPACE_UID", "DEVPLACE_TUNNEL_NAME", "DEVPLACE_TUNNEL_DOMAIN", "DEVPLACE_TUNNEL_MANIFEST", "VSCODE_PROXY_URI", "DEVPLACE_EDITOR_PORT", "DEVPLACE_QUOTA_DISK_MB", "DEVPLACE_RETENTION_DAYS", ): assert key in env, key assert all(isinstance(value, str) for value in env.values()) assert "{{port}}" in env["VSCODE_PROXY_URI"] def test_non_workspace_instance_gets_no_workspace_env(): from devplacepy.services.containers import api env = api.workspace_env({"is_workspace": 0}, "https://example.test") assert env == {"DEVPLACE_WORKSPACE": ""} def test_devii_workspace_tools_are_role_gated(): from devplacepy.services.devii.registry import CATALOG names = {a.name for a in CATALOG.actions if a.handler == "workspace"} admin_only = { a.name for a in CATALOG.actions if a.handler == "workspace" and a.requires_admin } guest = {s["function"]["name"] for s in CATALOG.tool_schemas_for(False, False)} member = {s["function"]["name"] for s in CATALOG.tool_schemas_for(True, False)} admin = {s["function"]["name"] for s in CATALOG.tool_schemas_for(True, True)} assert not (names & guest) assert not ((names & member) & admin_only) assert names <= admin def _idle_for(minutes: float) -> str: from datetime import datetime, timedelta, timezone return ( datetime.now(timezone.utc) - timedelta(minutes=minutes) ).isoformat() def _lifecycle(rows, stop="auto", delete="auto"): from devplacepy.services.containers.workspace_service import WorkspaceService WorkspaceService()._advance_lifecycle( rows, {"workspace_auto_stop": stop, "workspace_auto_delete": delete} ) def _reload(uid: str) -> dict: return store.get_instance(uid) def test_resuming_restarts_the_idle_clock(): instance = _instance(status="stopped", desired_state="stopped") store.update_instance(instance["uid"], {"last_active_at": _idle_for(600)}) resumed = provision.resume(_reload(instance["uid"])) assert resumed["desired_state"] == "running" running = dict(resumed, status="running") _lifecycle([running]) assert _reload(instance["uid"])["desired_state"] == "running" def test_idle_workspace_is_warned_then_stopped(): instance = _instance(status="running", desired_state="running") store.update_instance(instance["uid"], {"last_active_at": _idle_for(50)}) _lifecycle([_reload(instance["uid"])]) warned = _reload(instance["uid"]) assert warned["idle_warned_at"] assert warned["desired_state"] == "running" store.update_instance(instance["uid"], {"last_active_at": _idle_for(90)}) _lifecycle([_reload(instance["uid"])]) stopped = _reload(instance["uid"]) assert stopped["desired_state"] == "stopped" assert not stopped["idle_warned_at"] def test_fresh_workspace_is_left_alone(): instance = _instance(status="running", desired_state="running") store.update_instance(instance["uid"], {"last_active_at": _idle_for(1)}) _lifecycle([_reload(instance["uid"])]) fresh = _reload(instance["uid"]) assert fresh["desired_state"] == "running" assert not fresh["idle_warned_at"] def test_auto_stop_off_never_stops_an_idle_workspace(): instance = _instance(status="running", desired_state="running") store.update_instance(instance["uid"], {"last_active_at": _idle_for(900)}) _lifecycle([_reload(instance["uid"])], stop="off") assert _reload(instance["uid"])["desired_state"] == "running" def test_suspended_workspace_is_skipped_entirely(): instance = _instance(status="running", desired_state="running") store.update_instance( instance["uid"], { "last_active_at": _idle_for(60 * 24 * 400), "suspended_at": "2026-01-01T00:00:00+00:00", }, ) _lifecycle([_reload(instance["uid"])]) survivor = _reload(instance["uid"]) assert survivor is not None assert survivor["desired_state"] == "running" def test_workspace_without_activity_is_never_touched(): instance = _instance(status="running", desired_state="running") store.update_instance(instance["uid"], {"last_active_at": ""}) _lifecycle([_reload(instance["uid"])]) assert _reload(instance["uid"])["desired_state"] == "running" def test_stopped_workspace_past_retention_is_deleted_with_its_tunnels(): instance = _instance(status="stopped", desired_state="stopped") tunnels.create(instance, "web", 8080, OWNER) store.update_instance( instance["uid"], {"last_active_at": _idle_for(60 * 24 * 20)} ) _lifecycle([_reload(instance["uid"])]) assert _reload(instance["uid"]) is None for row in tunnels.list_for_instance(instance["uid"]): assert row["status"] == tunnels.STATUS_SUSPENDED def test_running_workspace_is_never_deleted_by_retention(): instance = _instance(status="running", desired_state="running") store.update_instance( instance["uid"], {"last_active_at": _idle_for(60 * 24 * 400)} ) _lifecycle([_reload(instance["uid"])], stop="off") assert _reload(instance["uid"]) is not None def test_auto_delete_off_keeps_an_expired_workspace(): instance = _instance(status="stopped", desired_state="stopped") store.update_instance( instance["uid"], {"last_active_at": _idle_for(60 * 24 * 400)} ) _lifecycle([_reload(instance["uid"])], delete="off") assert _reload(instance["uid"]) is not None def test_opening_a_workspace_publishes_the_editor_tunnel_automatically(): project = _project() user = {"uid": OWNER, "username": "owner"} instance = run_async(provision.ensure(project, user)) rows = tunnels.list_for_instance(instance["uid"]) assert len(rows) == 1 editor = rows[0] assert editor["container_port"] == api.EDITOR_DEFAULT_PORT assert editor["hostname"].startswith(f"{api.EDITOR_DEFAULT_PORT}-") assert editor["status"] == tunnels.STATUS_PENDING assert provision.is_editor_tunnel(instance, editor) assert instance["editor_password"] def test_active_editor_tunnel_notifies_the_owner_with_url_and_password(): from devplacepy.services.containers.workspace import certs project = _project() instance = run_async(provision.ensure(project, {"uid": OWNER, "username": "o"})) row = tunnels.list_for_instance(instance["uid"])[0] try: certs.announce(row, row["hostname"]) sent = list( get_table("notifications").find( user_uid=OWNER, type="workspace", order_by=["-id"] ) ) assert sent, "owner was not notified" message = sent[0]["message"] assert f"https://{row['hostname']}" in message assert instance["editor_password"] in message assert sent[0]["target_url"].endswith("/workspace") finally: get_table("notifications").delete(user_uid=OWNER) def test_editor_password_is_pronounceable_and_eight_chars(): seen = {api.generate_editor_password() for _ in range(50)} assert len(seen) > 40 for password in seen: assert len(password) == 8 assert password.isalpha() and password.islower() for index, char in enumerate(password): expected = ( api.EDITOR_PASSWORD_CONSONANTS if index % 2 == 0 else api.EDITOR_PASSWORD_VOWELS ) assert char in expected def test_editor_runs_with_password_auth_and_an_eight_char_secret(): from devplacepy.services.containers import api instance = _instance() password = api.ensure_editor_password(instance) assert len(password) == api.EDITOR_PASSWORD_LENGTH == 8 assert password.isalnum() command = api.editor_command(store.get_instance(instance["uid"])) assert "--auth" in command assert command[command.index("--auth") + 1] == "password" assert "none" not in command def test_editor_password_is_stable_and_reaches_the_container_env(): from devplacepy.services.containers import api instance = _instance() first = api.ensure_editor_password(instance) again = api.ensure_editor_password(store.get_instance(instance["uid"])) assert first == again env = api.pravda_env(store.get_instance(instance["uid"])) assert env["PASSWORD"] == first def test_every_workspace_launch_gets_a_password_even_if_never_provisioned(): from devplacepy.services.containers import api instance = _instance() store.update_instance(instance["uid"], {"editor_password": ""}) spec = api.run_spec_for(store.get_instance(instance["uid"]), "ppy:latest") assert len(spec.env["PASSWORD"]) == 8 assert store.get_instance(instance["uid"])["editor_password"] == spec.env["PASSWORD"] def test_non_workspace_instance_never_gets_an_editor_password(): from devplacepy.services.containers import api instance = _instance(is_workspace=0) spec = api.run_spec_for(store.get_instance(instance["uid"]), "ppy:latest") assert not spec.env.get("PASSWORD") def test_awaiting_certificate_only_returns_pending_present_tunnels(): instance = _instance() tunnels.create(instance, "web", 8080, OWNER) tunnels.create(instance, "api", 9090, OWNER) active = tunnels.list_for_instance(instance["uid"])[0] tunnels.update(active["uid"], {"status": tunnels.STATUS_ACTIVE}) waiting = [row["uid"] for row in tunnels.awaiting_certificate()] assert active["uid"] not in waiting assert len(waiting) == 1 absent = tunnels.get(waiting[0]) tunnels.mark_absent(absent["uid"]) assert not tunnels.awaiting_certificate() def test_certificate_phase_marks_active_on_success_and_failed_on_error(monkeypatch): from devplacepy.services.containers.workspace import certs from devplacepy.services.containers.workspace_service import WorkspaceService instance = _instance() tunnels.create(instance, "web", 8080, OWNER) row = tunnels.awaiting_certificate()[0] service = WorkspaceService() monkeypatch.setattr(certs, "configured", lambda: True) async def _ok(hostname): return None monkeypatch.setattr(certs, "issue", _ok) run_async(service._issue_tunnel_certificates()) assert tunnels.get(row["uid"])["status"] == tunnels.STATUS_ACTIVE tunnels.update(row["uid"], {"status": tunnels.STATUS_PENDING}) async def _boom(hostname): raise certs.CertError("molohttp cert issue failed (503): upstream down") monkeypatch.setattr(certs, "issue", _boom) run_async(service._issue_tunnel_certificates()) failed = tunnels.get(row["uid"]) assert failed["status"] == tunnels.STATUS_FAILED assert "503" in failed["last_error"] def test_certificate_phase_is_a_noop_without_molohttp_credentials(monkeypatch): from devplacepy.services.containers.workspace import certs from devplacepy.services.containers.workspace_service import WorkspaceService instance = _instance() tunnels.create(instance, "web", 8080, OWNER) row = tunnels.awaiting_certificate()[0] monkeypatch.setattr(certs, "configured", lambda: False) run_async(WorkspaceService()._issue_tunnel_certificates()) assert tunnels.get(row["uid"])["status"] == tunnels.STATUS_PENDING def test_workspace_controller_always_returns_a_json_string(): from devplacepy.services.devii.registry import CATALOG from devplacepy.services.devii.workspace.controller import WorkspaceController controller = WorkspaceController("user", OWNER, admin=True) names = [a.name for a in CATALOG.actions if a.handler == "workspace"] assert names for name in names + ["workspace_does_not_exist"]: result = run_async(controller.dispatch(name, {})) assert isinstance(result, str), f"{name} returned {type(result).__name__}" json.loads(result) def test_every_confirm_gated_tool_declares_a_confirm_param(): from devplacepy.services.devii.actions.dispatcher import CONFIRM_REQUIRED from devplacepy.services.devii.registry import CATALOG for action in CATALOG.actions: if action.handler != "workspace" or action.name not in CONFIRM_REQUIRED: continue assert any(p.name == "confirm" for p in action.params), action.name def test_tunnel_host_detection_never_matches_the_site(): assert naming.is_tunnel_host("abc." + naming.domain()) assert not naming.is_tunnel_host("pravda.education") assert not naming.is_tunnel_host("") def _seeded_user(username: str) -> dict: from devplacepy.database import db, refresh_snapshot refresh_snapshot() rows = list( db.query( "SELECT uid, username, api_key FROM users WHERE username = :username", username=username, ) ) assert rows, f"user {username} was not created by the server" return rows[0] def _fresh_member() -> dict: import time as _time import requests name = f"wsmem{int(_time.time() * 1000)}" requests.post( f"{BASE_URL}/auth/signup", data={ "username": name, "email": f"{name}@t.dev", "password": "secret123", "confirm_password": "secret123", }, allow_redirects=True, timeout=HTTP_TIMEOUT, ) return _seeded_user(name) def _http_project(owner_uid: str, title: str) -> dict: from devplacepy.utils import generate_uid, make_combined_slug uid = generate_uid() row = { "uid": uid, "user_uid": owner_uid, "title": title, "description": "workspace host project", "slug": make_combined_slug(title, uid), "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, } get_table("projects").insert(row) _CREATED_PROJECTS.append(uid) return row _CREATED_PROJECTS: list = [] @pytest.fixture(autouse=True) def _drop_created_projects(): yield while _CREATED_PROJECTS: get_table("projects").delete(uid=_CREATED_PROJECTS.pop()) def _await_workspaces_enabled(slug: str, key: str) -> bool: import time as _time import requests deadline = _time.time() + 10.0 headers = {"Accept": "application/json", "X-API-KEY": key} while _time.time() < deadline: body = requests.get( f"{BASE_URL}/projects/{slug}", headers=headers, timeout=HTTP_TIMEOUT ).json() if body.get("viewer_can_workspace"): return True _time.sleep(0.2) return False def _post(path: str, key: str, **kwargs): import requests return requests.post( f"{BASE_URL}{path}", headers={"Accept": "application/json", "X-API-KEY": key}, timeout=HTTP_TIMEOUT, **kwargs, ) def test_opening_a_workspace_over_quota_answers_400_not_500(app_server, seeded_db): owner = _seeded_user("bob_test") limit = quota.resolve(owner["uid"]).max_workspaces for index in range(limit): _instance( project_uid=f"proj-quota-{index}", name=f"ws-quota-{index}", workspace_owner_uid=owner["uid"], ) project = _http_project(owner["uid"], "WS Quota Http") assert _await_workspaces_enabled(project["slug"], owner["api_key"]) response = _post(f"/projects/{project['slug']}/workspace", owner["api_key"]) assert response.status_code == 400, response.text[:400] assert "limit" in response.json()["error"]["message"] assert provision.count_for_owner(owner["uid"]) == limit def test_opening_a_workspace_under_quota_is_not_refused(app_server, seeded_db): owner = _seeded_user("bob_test") project = _http_project(owner["uid"], "WS Room Http") assert _await_workspaces_enabled(project["slug"], owner["api_key"]) response = _post(f"/projects/{project['slug']}/workspace", owner["api_key"]) assert response.status_code != 500, response.text[:400] assert response.status_code != 400 or "limit" not in response.json()["error"]["message"] def test_tunnel_creation_rejects_a_zero_port_with_400(app_server, seeded_db): owner = _seeded_user("bob_test") project = _http_project(owner["uid"], "WS Port Http") _instance(project_uid=project["uid"], workspace_owner_uid=owner["uid"]) assert _await_workspaces_enabled(project["slug"], owner["api_key"]) response = _post( f"/projects/{project['slug']}/workspace/tunnels", owner["api_key"], data={"label": "web", "container_port": "0"}, ) assert response.status_code == 400, response.text[:400] assert "container_port" in response.json()["error"]["message"] def test_tunnel_limit_answers_400(app_server, seeded_db): owner = _seeded_user("bob_test") project = _http_project(owner["uid"], "WS Tunnel Cap Http") instance = _instance(project_uid=project["uid"], workspace_owner_uid=owner["uid"]) limit = quota.resolve(owner["uid"], instance).max_tunnels for port in range(9000, 9000 + limit): tunnels.create(instance, f"t{port}", port, owner["uid"]) assert _await_workspaces_enabled(project["slug"], owner["api_key"]) response = _post( f"/projects/{project['slug']}/workspace/tunnels", owner["api_key"], data={"label": "over", "container_port": "9999"}, ) assert response.status_code == 400, response.text[:400] assert "tunnel limit reached" in response.json()["error"]["message"] assert tunnels.count_for_instance(instance["uid"]) == limit def test_editor_proxy_denies_a_stranger_with_403(app_server, seeded_db): import requests owner = _seeded_user("bob_test") stranger = _fresh_member() project = _http_project(owner["uid"], "WS Editor Http") instance = _instance(project_uid=project["uid"], workspace_owner_uid=owner["uid"]) response = requests.get( f"{BASE_URL}/projects/{project['slug']}/containers/instances/{instance['uid']}/code/", headers={"Accept": "application/json", "X-API-KEY": stranger["api_key"]}, timeout=HTTP_TIMEOUT, ) assert response.status_code == 403, response.text[:400] def test_admin_suspend_without_a_reason_answers_400(app_server, seeded_db): admin = _seeded_user("alice_test") owner = _seeded_user("bob_test") project = _http_project(owner["uid"], "WS Suspend Http") instance = _instance(project_uid=project["uid"], workspace_owner_uid=owner["uid"]) response = _post( f"/admin/workspaces/{instance['uid']}/suspend", admin["api_key"], data={"reason": " "}, ) assert response.status_code == 400, response.text[:400] assert "reason" in response.json()["error"]["message"] assert not store.get_instance(instance["uid"])["suspended_at"] def test_admin_quota_rule_without_an_owner_answers_400(app_server, seeded_db): admin = _seeded_user("alice_test") response = _post( "/admin/workspaces/quota", admin["api_key"], data={"owner_id": ""} ) assert response.status_code == 400, response.text[:400] assert "owner_id" in response.json()["error"]["message"] def _certified(project: dict, monkeypatch, issue) -> dict: from devplacepy.services.containers.workspace import certs monkeypatch.setattr(certs, "configured", lambda: True) monkeypatch.setattr(certs, "issue", issue) async def _open_and_drain() -> dict: import asyncio instance = await provision.ensure(project, {"uid": OWNER, "username": "o"}) pending = list(provision._pending_certificates) if pending: await asyncio.gather(*pending, return_exceptions=True) return instance return run_async(_open_and_drain()) def test_opening_a_workspace_certifies_its_editor_tunnel_without_waiting_for_a_tick( monkeypatch, ): ordered = [] async def _ok(hostname: str) -> None: ordered.append(hostname) instance = _certified(_project(), monkeypatch, _ok) row = tunnels.list_for_instance(instance["uid"])[0] assert ordered == [row["hostname"]], "the editor certificate was not ordered eagerly" assert row["status"] == tunnels.STATUS_ACTIVE assert not tunnels.awaiting_certificate() def test_eagerly_certified_editor_tunnel_still_announces_url_and_password(monkeypatch): async def _ok(hostname: str) -> None: return None instance = _certified(_project(), monkeypatch, _ok) row = tunnels.list_for_instance(instance["uid"])[0] try: sent = list( get_table("notifications").find( user_uid=OWNER, type="workspace", order_by=["-id"] ) ) assert sent, "owner was not notified after eager issuance" assert f"https://{row['hostname']}" in sent[0]["message"] assert instance["editor_password"] in sent[0]["message"] finally: get_table("notifications").delete(user_uid=OWNER) def test_an_eager_certificate_failure_leaves_the_tunnel_failed_not_pending(monkeypatch): from devplacepy.services.containers.workspace import certs async def _boom(hostname: str) -> None: raise certs.CertError("molohttp cert issue failed (503): upstream down") instance = _certified(_project(), monkeypatch, _boom) row = tunnels.list_for_instance(instance["uid"])[0] assert row["status"] == tunnels.STATUS_FAILED assert "upstream down" in row["last_error"] assert not tunnels.awaiting_certificate() def test_no_certificate_is_ordered_when_molohttp_is_unconfigured(monkeypatch): from devplacepy.services.containers.workspace import certs ordered = [] async def _ok(hostname: str) -> None: ordered.append(hostname) monkeypatch.setattr(certs, "configured", lambda: False) monkeypatch.setattr(certs, "issue", _ok) instance = run_async(provision.ensure(_project(), {"uid": OWNER, "username": "o"})) assert ordered == [] row = tunnels.list_for_instance(instance["uid"])[0] assert row["status"] == tunnels.STATUS_PENDING assert [r["uid"] for r in tunnels.awaiting_certificate()] == [row["uid"]]