# retoor import re import time from uuid import uuid4 import pytest import requests from playwright.sync_api import expect from devplacepy.database import get_table, set_setting from devplacepy.services.containers import store from devplacepy.services.containers.workspace import flags, naming from devplacepy.utils import make_combined_slug from tests.conftest import BASE_URL @pytest.fixture(autouse=True) def _workspaces_on(): previous = None row = get_table("site_settings").find_one(key="workspace_enabled") if row: previous = row.get("value") set_setting("workspace_enabled", "1") try: yield finally: set_setting("workspace_enabled", previous if previous is not None else "0") instances = get_table("instances") created = [r["uid"] for r in instances.find(is_workspace=1)] for uid in created: get_table("tunnels").delete(instance_uid=uid) get_table("workspace_flags").delete(instance_uid=uid) instances.delete(uid=uid) def _row_for(user: dict) -> dict: return get_table("users").find_one(username=user["username"]) def _project_for(owner_uid: str, title: str = "WS Project") -> dict: uid = str(uuid4()) slug = make_combined_slug(title, uid) row = { "uid": uid, "user_uid": owner_uid, "title": title, "description": "workspace host project", "slug": slug, "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) return row def _workspace_for(project: dict, owner_uid: str, **overrides) -> dict: payload = { "project_uid": project["uid"], "name": "ws-e2e", "status": "running", "desired_state": "running", "is_workspace": 1, "workspace_owner_uid": owner_uid, "tunnel_name": naming.generate(), "ports_json": '[{"host": 20777, "container": 8080, "proto": "tcp"}]', } payload.update(overrides) return store.create_instance(payload) 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") page.goto( f"{BASE_URL}/projects/{project['slug']}", wait_until="domcontentloaded" ) page.locator(".project-actions-more").click() entry = page.locator(".context-menu-item:has-text('Workspace')") entry.wait_for(state="visible") entry.click() page.wait_for_url( f"{BASE_URL}/projects/{project['slug']}/workspace", wait_until="domcontentloaded", ) page.locator(".workspace-page").wait_for(state="visible") def test_project_page_hides_the_workspace_entry_point_from_a_non_owner(bob): page, user = bob project = _project_for(str(uuid4()), "WS Entry Foreign") page.goto( f"{BASE_URL}/projects/{project['slug']}", wait_until="domcontentloaded" ) page.locator(".project-actions-more").click() 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): page, user = alice row = _row_for(user) project = _project_for(row["uid"], "WS Direct") instance = _workspace_for(project, row["uid"]) page.goto( f"{BASE_URL}/projects/{project['slug']}", wait_until="domcontentloaded" ) button = page.locator(".project-detail-actions a:has-text('VS Code')") button.wait_for(state="visible") expect(button).to_have_attribute("target", "_blank") expect(button).to_have_attribute( "href", f"/projects/{project['slug']}/containers/instances/{instance['uid']}/code/", ) def test_direct_vscode_button_is_absent_while_the_workspace_is_stopped(alice): page, user = alice row = _row_for(user) project = _project_for(row["uid"], "WS Stopped") _workspace_for(project, row["uid"], status="stopped", desired_state="stopped") 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:has-text('VS Code')").count() def _await_workspace_flag(slug: str, key: str, expected: bool) -> bool: deadline = time.time() + 10.0 url = f"{BASE_URL}/projects/{slug}" headers = {"Accept": "application/json", "X-API-KEY": key} while time.time() < deadline: body = requests.get(url, headers=headers).json() if body["viewer_can_workspace"] is expected: return True time.sleep(0.2) return False def test_project_page_hides_the_workspace_entry_point_when_disabled(alice): page, user = alice row = _row_for(user) project = _project_for(row["uid"], "WS Entry Off") set_setting("workspace_enabled", "0") try: assert _await_workspace_flag(project["slug"], row["api_key"], False) page.goto( f"{BASE_URL}/projects/{project['slug']}", wait_until="domcontentloaded" ) page.locator(".project-actions-more").click() assert not page.locator(".context-menu-item:has-text('Workspace')").count() finally: set_setting("workspace_enabled", "1") assert _await_workspace_flag(project["slug"], row["api_key"], True) def test_workspace_page_offers_creation_to_owner(alice): page, user = alice project = _project_for(_row_for(user)["uid"]) page.goto( f"{BASE_URL}/projects/{project['slug']}/workspace", wait_until="domcontentloaded", ) page.locator(".workspace-page").wait_for(state="visible") expect(page.locator("button:has-text('Open workspace')")).to_be_visible() def test_workspace_page_shows_state_quota_and_tunnel_form(alice): page, user = alice project = _project_for(_row_for(user)["uid"], "WS Detail") _workspace_for(project, _row_for(user)["uid"]) page.goto( f"{BASE_URL}/projects/{project['slug']}/workspace", wait_until="domcontentloaded", ) page.locator(".workspace-summary").wait_for(state="visible") expect(page.locator("[data-workspace-status]")).to_contain_text("running") expect(page.locator(".workspace-meter").first).to_be_visible() expect(page.locator(".workspace-tunnel-form")).to_be_visible() expect(page.locator(".workspace-help")).to_contain_text("sudo") def test_owner_can_add_and_remove_a_tunnel(alice): page, user = alice project = _project_for(_row_for(user)["uid"], "WS Tunnel") _workspace_for(project, _row_for(user)["uid"]) url = f"{BASE_URL}/projects/{project['slug']}/workspace" page.goto(url, wait_until="domcontentloaded") page.locator(".workspace-tunnel-form input[name='container_port']").fill("8080") page.locator(".workspace-tunnel-form button:has-text('Add tunnel')").click() page.wait_for_url(url, wait_until="domcontentloaded") tunnel = page.locator(".workspace-tunnel").first tunnel.wait_for(state="visible") expect(tunnel).to_contain_text(naming.domain()) page.locator(".workspace-tunnel button:has-text('Remove')").first.click() page.locator(".dialog-confirm").wait_for(state="visible") page.locator(".dialog-confirm").click() page.wait_for_url(url, wait_until="domcontentloaded") expect(page.locator(".workspace-tunnel-list")).to_contain_text("No tunnels yet") def test_suspended_workspace_shows_reason_to_owner(alice): page, user = alice project = _project_for(_row_for(user)["uid"], "WS Suspended") instance = _workspace_for(project, _row_for(user)["uid"]) store.update_instance( instance["uid"], {"suspended_at": "2026-01-01T00:00:00+00:00", "flag_reason": "sustained cpu"}, ) page.goto( f"{BASE_URL}/projects/{project['slug']}/workspace", wait_until="domcontentloaded", ) banner = page.locator(".workspace-suspended") banner.wait_for(state="visible") expect(banner).to_contain_text("sustained cpu") def test_open_flag_is_visible_to_the_owner(alice): page, user = alice project = _project_for(_row_for(user)["uid"], "WS Flagged") instance = _workspace_for(project, _row_for(user)["uid"]) flags.raise_flag( store.get_instance(instance["uid"]), flags.KIND_EGRESS, "warn", "egress above the hourly ceiling", 2048.0, 1024.0, ) page.goto( f"{BASE_URL}/projects/{project['slug']}/workspace", wait_until="domcontentloaded", ) flag = page.locator(".workspace-flag").first flag.wait_for(state="visible") expect(flag).to_contain_text("egress above the hourly ceiling") def test_guest_cannot_reach_the_workspace_page(page): project = _project_for(str(uuid4()), "WS Guest") response = requests.get( f"{BASE_URL}/projects/{project['slug']}/workspace", allow_redirects=False, ) assert response.status_code in (303, 401, 404) def test_non_owner_member_is_refused(bob): page, user = bob project = _project_for(str(uuid4()), "WS Foreign") page.goto( f"{BASE_URL}/projects/{project['slug']}/workspace", wait_until="domcontentloaded", ) assert not page.locator(".workspace-summary").count() def test_admin_console_lists_and_suspends_a_workspace(alice): page, user = alice project = _project_for(_row_for(user)["uid"], "WS Admin") instance = _workspace_for(project, _row_for(user)["uid"]) page.goto(f"{BASE_URL}/admin/workspaces", wait_until="domcontentloaded") row = page.locator(f"tr[data-workspace-uid='{instance['uid']}']") row.wait_for(state="visible") expect(row).to_contain_text("ws-e2e") row.locator("input[name='reason']").fill("policy breach") row.locator("button:has-text('Suspend')").click() page.wait_for_url(f"{BASE_URL}/admin/workspaces", wait_until="domcontentloaded") refreshed = store.get_instance(instance["uid"]) assert refreshed["suspended_at"] row = page.locator(f"tr[data-workspace-uid='{instance['uid']}']") expect(row).to_contain_text("suspended") row.locator("button:has-text('Unsuspend')").click() page.wait_for_url(f"{BASE_URL}/admin/workspaces", wait_until="domcontentloaded") assert not store.get_instance(instance["uid"])["suspended_at"] def test_admin_console_raises_and_resolves_a_flag(alice): page, user = alice project = _project_for(_row_for(user)["uid"], "WS Flag Admin") instance = _workspace_for(project, _row_for(user)["uid"]) page.goto(f"{BASE_URL}/admin/workspaces", wait_until="domcontentloaded") row = page.locator(f"tr[data-workspace-uid='{instance['uid']}']") row.wait_for(state="visible") row.locator("input[name='detail']").fill("manual review") row.locator("button:has-text('Flag')").click() page.wait_for_url(f"{BASE_URL}/admin/workspaces", wait_until="domcontentloaded") assert flags.list_flags(instance_uid=instance["uid"]) page.locator("button:has-text('Resolve')").first.click() page.wait_for_url(f"{BASE_URL}/admin/workspaces", wait_until="domcontentloaded") assert not flags.list_flags(instance_uid=instance["uid"]) def test_workspaces_sidebar_link_is_present_for_admin(alice): page, user = alice page.goto(f"{BASE_URL}/admin/workspaces", wait_until="domcontentloaded") link = page.locator(".sidebar-link:has-text('Workspaces')") link.wait_for(state="visible") expect(link).to_have_class(re.compile("active"))