feat: add POST /api/sessions endpoint for new session creation
This commit is contained in:
Binary file not shown.
Binary file not shown.
+39
-1
@@ -14,6 +14,7 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import pwd
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Literal
|
||||
@@ -25,7 +26,7 @@ from websockets.typing import Subprotocol
|
||||
from fastapi import FastAPI, Form, HTTPException, Request, WebSocket
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, field_validator
|
||||
from starlette.responses import RedirectResponse
|
||||
|
||||
from muxplex.auth import (
|
||||
@@ -268,6 +269,18 @@ class HeartbeatPayload(BaseModel):
|
||||
last_interaction_at: float
|
||||
|
||||
|
||||
class CreateSessionPayload(BaseModel):
|
||||
name: str
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def name_must_not_be_blank(cls, v: str) -> str:
|
||||
stripped = v.strip()
|
||||
if not stripped:
|
||||
raise ValueError("name must not be empty or whitespace")
|
||||
return stripped
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Frontend directory
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -323,6 +336,31 @@ async def get_sessions() -> list[dict]:
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/api/sessions")
|
||||
async def create_session(payload: CreateSessionPayload) -> dict:
|
||||
"""Create a new tmux session using the new_session_template from settings.
|
||||
|
||||
Substitutes {name} in the template with the validated payload name, then
|
||||
runs the command as a fire-and-forget subprocess (stdout/stderr discarded).
|
||||
|
||||
Returns {name: name} regardless of outcome.
|
||||
Raises HTTP 422 if name is empty or whitespace (handled by Pydantic).
|
||||
"""
|
||||
name = payload.name
|
||||
settings = load_settings()
|
||||
command = settings["new_session_template"].replace("{name}", name)
|
||||
try:
|
||||
subprocess.Popen(
|
||||
command,
|
||||
shell=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except Exception:
|
||||
_log.warning("Failed to launch new-session command: %r", command)
|
||||
return {"name": name}
|
||||
|
||||
|
||||
@app.post("/api/sessions/{name}/connect")
|
||||
async def connect_session(name: str) -> dict:
|
||||
"""Connect to a tmux session via ttyd.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -966,3 +966,57 @@ def test_patch_settings_ignores_unknown_keys(client, tmp_path, monkeypatch):
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "unknown_key" not in data
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /api/sessions (create new session)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_create_session_returns_200_with_name(client, monkeypatch):
|
||||
"""POST /api/sessions with valid name returns 200 with {name: name}."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
mock_popen = MagicMock()
|
||||
monkeypatch.setattr("muxplex.main.subprocess.Popen", mock_popen)
|
||||
|
||||
response = client.post("/api/sessions", json={"name": "my-project"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == "my-project"
|
||||
|
||||
|
||||
def test_create_session_substitutes_name_in_template(client, tmp_path, monkeypatch):
|
||||
"""POST /api/sessions substitutes {name} with actual name in new_session_template."""
|
||||
import json
|
||||
|
||||
import muxplex.settings as settings_mod
|
||||
|
||||
settings_path = tmp_path / "settings.json"
|
||||
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_path)
|
||||
settings_path.write_text(json.dumps({"new_session_template": "echo {name}"}))
|
||||
|
||||
popen_calls = []
|
||||
|
||||
def mock_popen(cmd, **kwargs):
|
||||
popen_calls.append(cmd)
|
||||
return object()
|
||||
|
||||
monkeypatch.setattr("muxplex.main.subprocess.Popen", mock_popen)
|
||||
|
||||
response = client.post("/api/sessions", json={"name": "my-project"})
|
||||
assert response.status_code == 200
|
||||
assert len(popen_calls) == 1
|
||||
assert popen_calls[0] == "echo my-project"
|
||||
|
||||
|
||||
def test_create_session_rejects_empty_name(client):
|
||||
"""POST /api/sessions with empty name returns 422."""
|
||||
response = client.post("/api/sessions", json={"name": ""})
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_create_session_rejects_missing_name(client):
|
||||
"""POST /api/sessions with missing name returns 422."""
|
||||
response = client.post("/api/sessions", json={})
|
||||
assert response.status_code == 422
|
||||
|
||||
Reference in New Issue
Block a user