From 19ec5c02a580a626bacda819612a1c257852fbc5 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Sun, 29 Mar 2026 22:54:23 -0700 Subject: [PATCH] feat: add POST /api/sessions endpoint for new session creation --- muxplex/__pycache__/main.cpython-312.pyc | Bin 22837 -> 25496 bytes muxplex/__pycache__/settings.cpython-312.pyc | Bin 0 -> 2804 bytes muxplex/main.py | 40 ++++++++++++- .../test_api.cpython-312-pytest-9.0.2.pyc | Bin 93323 -> 106121 bytes ...test_settings.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 12020 bytes muxplex/tests/test_api.py | 54 ++++++++++++++++++ 6 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 muxplex/__pycache__/settings.cpython-312.pyc create mode 100644 muxplex/tests/__pycache__/test_settings.cpython-312-pytest-9.0.2.pyc diff --git a/muxplex/__pycache__/main.cpython-312.pyc b/muxplex/__pycache__/main.cpython-312.pyc index 1ebf0a87e7ba610020d79b0eed0d3f07ea65e2a7..517376b423a4b486eadfee71dd9f448f678358b0 100644 GIT binary patch delta 6518 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 W`L(HA=F@>&d_(-XX@Pg5xBmi{rw45S diff --git a/muxplex/__pycache__/settings.cpython-312.pyc b/muxplex/__pycache__/settings.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eee70813f3f38e6214eb74b24b88fc77eec55350 GIT binary patch literal 2804 zcmbVOO>7&-6`ox#|Fxn-8Hgpz(x6ctQ?Vo9||9eyqVn<6~#b- z46yU&=gr%qP7^dq`Yys6 zV$wuOC;h)S@+p#`31SkxWRL;qr^%q~!@NPV(C^#Sb3={g)FNf;ludD)P|T?iwzJG} z#d54=TA_}>C70n!ZKGPI8`CN94m>Od?!sl1U`ycN&P*3wr(`eB1g0Xa0$B6<~x{xF_61nTNQIM%ZzeF8Cn z^t)4aRC!fq%C6_QJG^c*9w9 z+`5C;=qAUu(~BZ;Z7(|#*uG~j=@~`0y6GhdrB&Hta;H5rgh1h)xuu_YNx%&Oplk@w zkdXJHW%z9F4bDSu3$B=VYYw@>mM1aL65a z@#f5mTcIyOQITk~B#9yOn4M)xyk+L<$tGiSGrrMR78Pbzy zw=Z=v*%sM(uQT?Yz1e$qE7szFN`D!Jscs7OWw);X_jweVWB0}W_-uQ8_EGx$Go(e$ zPNJ`q9^AU#jY8)Im$`iSy}5DpSvDo>@!`3Pk5)sg$EYwpckN70`uo5m4)r(mRf;0DMq; zVl!FqRSTkeipP%S;r{*pQr$ zl3Jz8b3Rz$+5kzRgpI%ymq-EXuv5~{lp@ok!C4r~NhcSom(bv8W*kr+o!E|a(*3Q8 zoeQ0jv0Z*Iw>{sQ-T8Sp3RBNxD4N|jPPUDc`^H$?7~2yM$s=RziE-hnF}QDxwvEyI zOMl7V%YPJm_?suEzyHbQ_URw}!6dh5~%q9 z18@{VsnWX(G}P17=t;3rr#-XL|8M zfw;v&!3U;-8$emClG^GvgPIM8Nz4OndQK^0f>dD5N literal 0 HcmV?d00001 diff --git a/muxplex/main.py b/muxplex/main.py index 80c6f67..7ad253d 100644 --- a/muxplex/main.py +++ b/muxplex/main.py @@ -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. diff --git a/muxplex/tests/__pycache__/test_api.cpython-312-pytest-9.0.2.pyc b/muxplex/tests/__pycache__/test_api.cpython-312-pytest-9.0.2.pyc index 23862f0025ab1b39be484af8414761293fbf5af8..909b7b5d2dda9bf15408a647f8a5501975663c1e 100644 GIT binary patch delta 9681 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 Btt|im diff --git a/muxplex/tests/__pycache__/test_settings.cpython-312-pytest-9.0.2.pyc b/muxplex/tests/__pycache__/test_settings.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2787a19cb79d967ad0d6af848d8c29a92525a4d4 GIT binary patch literal 12020 zcmeHN?Qa~%dEdR8dtDyi^hQb)xv?cmCs9Z1-HxRsOl>Q2Y}&2ZCMJw7mN!fCsC&cg zo+#3H2-6yfgCvO47_b}|DA7+6R22RT3glbSe&`*KEY(FmsEzWW{Ul2T4CqszXJ+5- zbbN7~05Oue+h=EHpV^(6-TBS$d1n4Cl}btwzLNUa+z0)V^lMDGA?!pR|1(5Bl&CZ< zQJIGF@^pxw!v&HJ$xpG6o8`Q5bKEp6D_5K|Gvy`qzgIp`O@rMq z*eNqd4Ty_tYC$jRb4I}^TAAcURnKOPvZWWZ#;GjJSq97Ls>N?ijoDn$pz6HAj7;)> zu-cPpV)wrE@+%X6`P!6r?d7SdtCLr**{OUHXpjRRcdeE_Sz0F(_msVLviC7)N?8pzq% z?*+JDZw+3M{|c@jhZiMRWpmBK*T};{)Q$OP3Oche>VjowLwAYLpkXWF>Mrn+?54!o zefAB>+!uIGSF0ALs**e6SWvTjJI$g@!<6j027}wz!0#KcCczRNYVvdms1c!&k3-AQ zleG|y(pWW=lX)s^;cMn0P!GVo_D~JG&v(y%Bhz?IUW%s^%a>aQ?(wwB46DM5rrJKl z)x~+Es1{3Nycjog06dvH@{`cz%Xj4E{-ejsOV6cE_3~vEqi52l9c6}D$y*FRqB|iw z3<@aDJn7a=KqZSSo&!b5JU5w6GW-U(ww?S&d5M4RI@K*h z%~W!EKstC%z62VkJ)oKMrAnT1Rc$U`n$h!GQ7;&FLNoPQfUq*NyC?XwbEV?TjFp(Z zM+5LI=$2*xd;%(3a3xLH0jP?t;A2ExPm6@mJsV7%H?i6RnzVN4+v|-HUq)5CI*AA4>}Wdt?3=0IgpHfEPRrfJZpN!n^aZ5O-t#8Ao1D zcE3>fEK3$l03x_!zZR~BX?PGaYlMdWfq8f8XY~0%8-a2rny>6a=uEyObx@Q=%$(wSGz!8NO zhpFPUKPlQ@k!rhXeHU+i>dW2wrDVE$`Aj=*6%00Km?{zts@~A^6-bnT`Ka#b%P^c- z7)~m7(kwAcD=}&?JEG^y^E$)V-~!ocAIJco1a_TfL!i-uT^9s)U6{0coQJ?XY?^=| zcVs)1GlRFwnJ(O=6^5TQ!#T@NE+XO5EaRqS6V5dvv{ERW0t*kL-Y|-1P&|v`2#9oo z;aAS^17|%b+VSeSAb2@=C6Me4I|c=U2zJrLjzI+DLBQDX(=f)nRuf&zEQWQk!I;K?XkF`}YSwI=K4wCl~8SUTh9tS|^ts+yjo%)&KrjLm8=)6RQ_N)RmFd ziw)((Cc*!PlOa+~o)Ymj)ECj91eNc3b>#@0m9b5N{|zTYq?$Y> z;%lfcqCvS%#-4^uq&S=={S&82E@!17WUe&4Oa4lN{MDb%X@o|7P()_iiBi?VAqggV zs0fA{sS@)i6rg9kS4!RyK*+I{PUz+$h%&AXg z*Ffq)vfBa&NW(P(Frq9!-(r{m{mC*zhfiNco>8<^Fd$4-FH)6{Q&Y9(!B&9TJ7deb zEzkIn!cIWfGaM1i6Rq?n`3@2yz!BA>Mj{J1l9L6*DI*wFvUWV3tl3HtsJ=ltn@@)s zVm&*E0x@VZh#(BVKRYT)vk}x$QE=$^3dY)?BN~8?Z{T(0AnXMc2n;6PF9-x990+(f zIeo+Fiw2#dVehbSLV>Sh$-e`!BHiylc8vW0&e7n`=cxRpb*#pw| z{-xCe_5Soa4%7Xc%Hf7`xK2huHW30KGO|hh{{|U(??sW(^rP!!q^=wm*=rd03wXLu zk64f7JV(9=$q9_o7U}|&Wm}>HV;GR3J-DOS{Z9Wxa;SnxRt?_})aIiAN_4;|>QkHW z=O=cw+rk-?hXu5p+k*RcxY_9=AP&j4;2T>zVx@Xxo9=B#;-a;h^1Sd0drJisRxV4(hV z!HFjugU^VaLNSiwIS`ZVG)Qojg>uETV__pP%@sM5JPYJAhO?4o6z_ys26WMoKHI`j z&kKfn-tI$pn^r1={V7nKrn`L_$|n(a0dEmOUTxY5mxF#zJ~fD8kC@NUm2>CV|8U{HC9)SZ4&%%I2j_<j zcNrp3?4B}QC+WH}yb2FUZxZ}(I2j_<Yj!g5y;cJDgHvzUH5d%sw?hs5_PMhF3zq0mlc!5SjlgzrW}^sA z&}22@%tldvmJHOR!+cqdy3cpdFdM;qzNDnPmM^uN?yk$xwba!)re}@WNy3 zY~X>y@fZL*pGtRP(Xn>0L;4`7^eDzSl|~xq09g>kc=P9hKj#foa~h|i?a%%k3S5K3 zTm`W`h~0Bd>iS@2os2tHG+4ZU2;ReaFo=J9@8MR&dIw6pE$QCE@8_54u+|Xp`)#l1 z8JTn|zn@1L1ExU-68ky)eqC*TKfi_^Ji<5R1e)T0zwS1_pI_!n(RzQq!-eEpqMF#o z6YRIIQ=fG91p8&a6nz4Ov}bvub+r||7?m=RQSf5ustLRk);F&gON&MDTo`%E-5D*L z_(B{wg;Q{~rM3NZl3ui-FH?aU8Thi<)n`(l0QY%_Ikaq{3w8%oyKwuR&&7Ni0%TP*agaLJC_QY zs*ipBUltof-)W}4i^w}rC!_COUX21kZW8=&h_pyFF(IO!gaWHk@3I^9E<*%L!6N(b z1tg(`Ucir*uM54+L9+Aq87kw@TtL0HKs_)7I~eT~9)x~Pp5uF>sv$m}Q1)u-ZB!wAqY5nNJF7LjrN17L4!~F(hOrDsxEAWrhp_1V$K5A< z6VIEkS719DFg?CLm4~f4&Nd!c3ZGe0+wJW_1r!j9T+8|?;In(?bCiO!&{@wm?e4q* zTi9@mA79|!+H0APbCPFA^)h%Hsj$GeT*!_I4BV3Y#RNe;W8!df$e&<-CpZG}rqL1@ z($F5QfZbrYPC;)|9Mhoi5*+5ggYdT6+$TwJ$2ABnARTFtBlV-_*U1GSQe8v!p;HYd zQzz#>c?JX!vrnFBDCaf_{x_Tqk!tdkh_9i(hz2F7z)gFGepFhya!={6?>Pdyd%&f5 zyPx|NkKXQY^(#U~z^@29f6(IyenmGs=vQ1PCj~tNQtJg=!Q;sD5F)V66}Pxb1sFxR zLA5HobOy$g=YR9PZk<<{H4lNRQm54`%k#Pc#sqd^ZtvIfam^j?=OsWpZ|Lh=9|OP8HP^;7kuJ%eJ+=My_CJa+z(se&^fM_J!QV91U9hQ zsY((3@urn2fGX3Wu-(&kQ}!C>D!3n4GiGO@7QXRhN5!@`*gB`@V5A9iDJ=GR@Vh1O z#w{C3#b`L^{(vy!SMHoZ;^HR@TVan?|U4ULc+nyyIz literal 0 HcmV?d00001 diff --git a/muxplex/tests/test_api.py b/muxplex/tests/test_api.py index 0c9880c..a6049de 100644 --- a/muxplex/tests/test_api.py +++ b/muxplex/tests/test_api.py @@ -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