← Files Dell Agentic WorkspaceARCHIVED FILE
tests/test_devspaces_mcp_server.py
11.3 KB · Nov 14, 2026 · 01:37 UTC
# SPDX-FileCopyrightText: Copyright (c) 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import importlib.util
import subprocess
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
SERVER_PATH = Path(__file__).parents[1] / "scripts" / "devspaces_mcp_server.py"
SPEC = importlib.util.spec_from_file_location("devspaces_mcp_server", SERVER_PATH)
SERVER = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = SERVER
SPEC.loader.exec_module(SERVER)
def settings(**overrides):
values = {
"kubeconfig": None,
"requested_context": None,
"allowed_contexts": (),
"allowed_namespaces": ("team-a",),
"enable_writes": False,
"enable_delete": False,
"instances_file": str(
Path(__file__).parents[1] / "config" / "instances.json"
),
"default_instance": None,
"oc_command": "oc",
}
values.update(overrides)
return SERVER.Settings(**values)
def bare_client(**overrides):
adapter = object.__new__(SERVER.DevSpacesClient)
adapter.settings = settings(**overrides)
adapter.namespaces = adapter.settings.allowed_namespaces
return adapter
def test_defaults_are_read_only(monkeypatch):
for name in (
"DEVSPACES_ENABLE_WRITES",
"DEVSPACES_ENABLE_DELETE",
"DEVSPACES_ALLOWED_NAMESPACES",
):
monkeypatch.delenv(name, raising=False)
actual = SERVER.Settings.from_env()
assert actual.enable_writes is False
assert actual.enable_delete is False
assert actual.allowed_namespaces == ()
def instance(name="primary", default=True):
return SERVER.DevSpacesInstance(
name=name,
display_name=name.title(),
api_url=f"https://api.{name}.example:6443",
ui_url=f"https://ui.{name}.example",
default=default,
)
def test_registry_resolves_named_and_default_instances():
registry = SERVER.InstanceRegistry(
(instance("primary", True), instance("secondary", False))
)
assert registry.resolve().name == "primary"
assert registry.resolve("secondary").api_url == "https://api.secondary.example:6443"
def test_registry_rejects_untrusted_instance_url():
with pytest.raises(ValueError, match="credential-free HTTPS"):
SERVER.DevSpacesInstance.from_dict(
{
"name": "bad",
"display_name": "Bad",
"api_url": "http://user:password@example.invalid",
"ui_url": "https://ui.example.invalid",
}
)
def test_authentication_status_returns_identity_but_no_command_output(monkeypatch):
registry = SERVER.InstanceRegistry((instance(),))
manager = SERVER.AuthenticationManager(settings(), registry)
responses = {
("whoami", "--show-server"): subprocess.CompletedProcess(
[], 0, "https://api.primary.example:6443\n", ""
),
("whoami",): subprocess.CompletedProcess([], 0, "user@example.com\n", ""),
("config", "current-context"): subprocess.CompletedProcess(
[], 0, "primary/user\n", ""
),
}
monkeypatch.setattr(manager, "_oc_path", lambda: "oc")
monkeypatch.setattr(
manager, "_run", lambda arguments, timeout=30: responses[tuple(arguments)]
)
result = manager.status()
assert result["authenticated"] is True
assert result["username"] == "user@example.com"
assert result["context"] == "primary/user"
assert "token" not in str(result).lower()
def test_authentication_initiates_registered_web_login(monkeypatch):
registry = SERVER.InstanceRegistry((instance(),))
manager = SERVER.AuthenticationManager(settings(), registry)
monkeypatch.setattr(
manager,
"status",
lambda instance_name=None: {
"authenticated": False,
"instance": instance().public(),
},
)
monkeypatch.setattr(manager, "_oc_path", lambda: "/managed/bin/oc")
calls = []
class Process:
def __init__(self, arguments, **kwargs):
calls.append((arguments, kwargs))
def poll(self):
return None
monkeypatch.setattr(SERVER.subprocess, "Popen", Process)
result = manager.authenticate()
assert calls[0][0] == [
"/managed/bin/oc",
"login",
"https://api.primary.example:6443",
"--web",
]
assert "shell" not in calls[0][1]
assert result["login_started"] is True
assert result["login_state"] == "waiting_for_browser"
def test_workspace_summary_excludes_spec_and_secrets():
workspace = {
"metadata": {
"name": "sample",
"namespace": "team-a",
"creationTimestamp": "2026-08-05T12:00:00Z",
},
"spec": {"started": True, "sensitive": "must-not-leak"},
"status": {"phase": "Running", "mainUrl": "https://example.invalid"},
}
result = SERVER.DevSpacesClient.summarize_workspace(workspace)
assert set(result) == {
"name",
"namespace",
"started",
"phase",
"message",
"main_url",
"created",
}
assert "must-not-leak" not in str(result)
def test_write_gate_fails_before_api_call():
adapter = bare_client()
with pytest.raises(PermissionError, match="writes are disabled"):
adapter.set_started("team-a", "sample", True)
def test_delete_requires_exact_confirmation_before_api_call():
adapter = bare_client(enable_delete=True)
with pytest.raises(ValueError, match="exactly match"):
adapter.delete_workspace("team-a", "sample", "different")
def test_namespace_boundary_is_exact():
adapter = bare_client()
adapter._authorize_namespace("team-a")
with pytest.raises(PermissionError, match="not authorized"):
adapter._authorize_namespace("team-b")
def test_workspace_pod_discovery_uses_only_workspace_labels():
adapter = bare_client()
calls = []
pod = SimpleNamespace(metadata=SimpleNamespace(name="sample-pod"))
class Core:
def list_namespaced_pod(self, namespace, label_selector):
calls.append((namespace, label_selector))
return SimpleNamespace(items=[pod])
adapter.core = Core()
result = adapter._workspace_pods("team-a", "sample")
assert [item.metadata.name for item in result] == ["sample-pod"]
assert calls == [
("team-a", f"{label}=sample") for label in SERVER.WORKSPACE_POD_LABELS
]
class Kubeconfig:
"""Synthetic kubeconfig whose active context can change, as `oc login` does."""
def __init__(self, active, names=("approved", "other")):
self.active = active
self.names = names
self.loaded = []
def list_kube_config_contexts(self, config_file=None):
return [{"name": name} for name in self.names], {"name": self.active}
def load_kube_config(self, config_file=None, context=None):
self.loaded.append(context)
def install_kubeconfig(monkeypatch, kubeconfig, **overrides):
monkeypatch.setattr(SERVER, "config", kubeconfig)
monkeypatch.setattr(SERVER, "SETTINGS", settings(**overrides))
monkeypatch.setattr(SERVER, "_client", None)
monkeypatch.setattr(SERVER, "_session_context", None)
monkeypatch.setattr(SERVER, "_startup_contexts", None)
monkeypatch.setattr(SERVER.DevSpacesClient, "__init__", lambda self, effective: setattr(self, "settings", effective))
@pytest.mark.parametrize("configured", [(), ("approved",)])
def test_login_context_outside_allowlist_is_blocked(monkeypatch, configured):
kubeconfig = Kubeconfig(active="approved")
install_kubeconfig(monkeypatch, kubeconfig, allowed_contexts=configured)
assert SERVER.allowed_contexts() == ("approved",)
# `oc login` to another cluster switches the active kubeconfig context.
kubeconfig.active = "other"
SERVER.activate_context("other")
with pytest.raises(PermissionError, match="outside the allowed contexts"):
SERVER.get_client()
assert SERVER.allowed_contexts() == ("approved",)
def test_login_context_inside_allowlist_is_used(monkeypatch):
install_kubeconfig(monkeypatch, Kubeconfig(active="other"), allowed_contexts=("approved", "other"))
SERVER.activate_context("approved")
adapter = SERVER.get_client()
assert adapter.settings.requested_context == "approved"
assert adapter.settings.allowed_contexts == ("approved", "other")
def test_authentication_status_flags_disallowed_context(monkeypatch):
install_kubeconfig(monkeypatch, Kubeconfig(active="approved"))
SERVER.activate_context('approved')
original = SERVER.get_client()
monkeypatch.setattr(SERVER.AUTHENTICATION, "status", lambda instance_name=None: {
"authenticated": True, "context": "other"})
result = SERVER.get_authentication_status()
assert result["context_allowed"] is False
assert "outside the allowed contexts" in result["reason"]
assert SERVER.get_client() is original
assert SERVER._session_context == 'approved'
def test_status_does_not_switch_to_another_allowed_cluster(monkeypatch):
install_kubeconfig(monkeypatch, Kubeconfig(active='approved'), allowed_contexts=('approved', 'other'))
SERVER.activate_context('approved')
original = SERVER.get_client()
monkeypatch.setattr(SERVER.AUTHENTICATION, 'status', lambda instance_name=None: {
'authenticated': True, 'context': 'other'})
assert SERVER.get_authentication_status()['context_allowed'] is True
assert SERVER._session_context == 'approved'
assert SERVER.get_client() is original
def test_kubernetes_stop_is_advertised_as_destructive():
import asyncio
tools = asyncio.run(SERVER.mcp.list_tools())
stop = next(tool for tool in tools if tool.name == 'stop_devspace')
assert stop.annotations.destructiveHint is True
assert stop.annotations.readOnlyHint is False
assert stop.annotations.idempotentHint is True
def test_client_constructor_never_widens_explicit_allowlist(monkeypatch):
kubeconfig = Kubeconfig(active="other")
monkeypatch.setattr(SERVER, "config", kubeconfig)
with pytest.raises(PermissionError, match="outside"):
SERVER.DevSpacesClient(settings(allowed_contexts=("approved",)))
assert kubeconfig.loaded == []
def test_allowlist_is_captured_at_startup_not_after_login(monkeypatch, tmp_path):
kubeconfig = tmp_path / "kubeconfig"
kubeconfig.write_text(
"apiVersion: v1\nkind: Config\ncurrent-context: approved\n"
"clusters: [{name: c, cluster: {server: 'https://api.approved.example:6443'}}]\n"
"users: [{name: u, user: {}}]\n"
"contexts: [{name: approved, context: {cluster: c, user: u}},"
" {name: other, context: {cluster: c, user: u}}]\n"
)
monkeypatch.setenv("DEVSPACES_KUBECONFIG", str(kubeconfig))
monkeypatch.delenv("DEVSPACES_ALLOWED_CONTEXTS", raising=False)
spec = importlib.util.spec_from_file_location("devspaces_startup", SERVER_PATH)
fresh = importlib.util.module_from_spec(spec)
monkeypatch.setitem(sys.modules, spec.name, fresh)
spec.loader.exec_module(fresh)
# A background `oc login` later switches the active context before any tool call.
kubeconfig.write_text(kubeconfig.read_text().replace("current-context: approved", "current-context: other"))
assert fresh.allowed_contexts() == ("approved",)
fresh.activate_context("other")
with pytest.raises(PermissionError, match="outside the allowed contexts"):
fresh.get_client()
SHA-256: ede42723cc58799205bb05a24482a502b13adb3e6d2a7f956506a20d167990d8