feat: customizable delete session command template
Symmetric with create template. Default: 'tmux kill-session -t {name}'.
Configurable in Settings → Commands tab (renamed from 'New Session').
DELETE /api/sessions/{name} reads template from settings, substitutes
{name}, runs synchronously via subprocess.run with 30s timeout.
Enables custom teardown workflows like:
amplifier-dev ~/dev/{name} --destroy
Changes:
- settings.py: add delete_session_template to DEFAULT_SETTINGS
- main.py: DELETE endpoint uses subprocess.run + template instead of run_tmux
- index.html: add delete template textarea + reset btn; rename tab to Commands
- app.js: DELETE_SESSION_DEFAULT_TEMPLATE constant, openSettings loads it,
bindStaticEventListeners wires input/reset; constant exported
Tests added:
- test_settings.py: 3 tests for default, load, and patch of delete_session_template
- test_api.py: 2 tests for custom/default template substitution via subprocess mock;
update existing test_delete_session_calls_kill_session to match new implementation
- test_frontend_html.py: 5 tests for textarea, placeholder, rows, reset btn, tab label
- test_app.mjs: 5 tests for constant definition, value, and wiring in openSettings/bindStaticEventListeners
This commit is contained in:
+30
-3
@@ -142,6 +142,7 @@ const DISPLAY_DEFAULTS = {
|
||||
notificationPermission: 'default',
|
||||
};
|
||||
const NEW_SESSION_DEFAULT_TEMPLATE = 'tmux new-session -d -s {name}';
|
||||
const DELETE_SESSION_DEFAULT_TEMPLATE = 'tmux kill-session -t {name}';
|
||||
|
||||
// ─── DOM helpers ──────────────────────────────────────────────────────────────
|
||||
function $(id) {
|
||||
@@ -1221,11 +1222,17 @@ function openSettings() {
|
||||
autoOpenEl.checked = ss && ss.auto_open !== undefined ? !!ss.auto_open : true;
|
||||
}
|
||||
|
||||
// New Session tab - populate template textarea
|
||||
// Commands tab - populate create template textarea
|
||||
const templateEl = $('setting-template');
|
||||
if (templateEl) {
|
||||
templateEl.value = (ss && ss.new_session_template) || NEW_SESSION_DEFAULT_TEMPLATE;
|
||||
}
|
||||
|
||||
// Commands tab - populate delete template textarea
|
||||
const deleteTemplateEl = $('setting-delete-template');
|
||||
if (deleteTemplateEl) {
|
||||
deleteTemplateEl.value = (ss && ss.delete_session_template) || DELETE_SESSION_DEFAULT_TEMPLATE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1687,7 +1694,7 @@ function bindStaticEventListeners() {
|
||||
});
|
||||
});
|
||||
|
||||
// New Session tab — template textarea with 500ms debounce
|
||||
// Commands tab — create template textarea with 500ms debounce
|
||||
var _templateDebounceTimer;
|
||||
on($('setting-template'), 'input', function() {
|
||||
clearTimeout(_templateDebounceTimer);
|
||||
@@ -1697,12 +1704,29 @@ function bindStaticEventListeners() {
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// New Session tab — reset button restores default template
|
||||
// Commands tab — create template reset button restores default
|
||||
on($('setting-template-reset'), 'click', function() {
|
||||
var el = $('setting-template');
|
||||
if (el) el.value = NEW_SESSION_DEFAULT_TEMPLATE;
|
||||
patchServerSetting('new_session_template', NEW_SESSION_DEFAULT_TEMPLATE);
|
||||
});
|
||||
|
||||
// Commands tab — delete template textarea with 500ms debounce
|
||||
var _deleteTemplateDebounceTimer;
|
||||
on($('setting-delete-template'), 'input', function() {
|
||||
clearTimeout(_deleteTemplateDebounceTimer);
|
||||
var val = this.value;
|
||||
_deleteTemplateDebounceTimer = setTimeout(function() {
|
||||
patchServerSetting('delete_session_template', val);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Commands tab — delete template reset button restores default
|
||||
on($('setting-delete-template-reset'), 'click', function() {
|
||||
var el = $('setting-delete-template');
|
||||
if (el) el.value = DELETE_SESSION_DEFAULT_TEMPLATE;
|
||||
patchServerSetting('delete_session_template', DELETE_SESSION_DEFAULT_TEMPLATE);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Test-only helpers ────────────────────────────────────────────────────────
|
||||
@@ -1798,6 +1822,9 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
createNewSession,
|
||||
// Kill session
|
||||
killSession,
|
||||
// Constants
|
||||
NEW_SESSION_DEFAULT_TEMPLATE,
|
||||
DELETE_SESSION_DEFAULT_TEMPLATE,
|
||||
// Test-only helpers
|
||||
_setCurrentSessions,
|
||||
_setViewMode,
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
<button class="settings-tab settings-tab--active" data-tab="display">Display</button>
|
||||
<button class="settings-tab" data-tab="sessions">Sessions</button>
|
||||
<button class="settings-tab" data-tab="notifications">Notifications</button>
|
||||
<button class="settings-tab" data-tab="new-session">New Session</button>
|
||||
<button class="settings-tab" data-tab="new-session">Commands</button>
|
||||
</nav>
|
||||
<div class="settings-content">
|
||||
<div class="settings-panel" data-tab="display">
|
||||
@@ -169,6 +169,12 @@
|
||||
<span class="settings-helper">{name} is replaced with the session name</span>
|
||||
<button id="setting-template-reset" class="settings-action-btn">Reset to default</button>
|
||||
</div>
|
||||
<div class="settings-field settings-field--column">
|
||||
<label class="settings-label" for="setting-delete-template">Delete session command</label>
|
||||
<textarea id="setting-delete-template" class="settings-textarea" rows="3" placeholder="tmux kill-session -t {name}"></textarea>
|
||||
<span class="settings-helper">{name} is replaced with the session name</span>
|
||||
<button id="setting-delete-template-reset" class="settings-action-btn">Reset to default</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2063,3 +2063,58 @@ test('pollSessions calls updateFaviconBadge', () => {
|
||||
'pollSessions must call updateFaviconBadge — update favicon on every poll cycle'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
// --- Delete session template (task: customizable delete command) ---
|
||||
|
||||
test('app.js defines DELETE_SESSION_DEFAULT_TEMPLATE constant', () => {
|
||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||
assert.ok(
|
||||
source.includes('DELETE_SESSION_DEFAULT_TEMPLATE'),
|
||||
'app.js must define DELETE_SESSION_DEFAULT_TEMPLATE constant'
|
||||
);
|
||||
});
|
||||
|
||||
test('DELETE_SESSION_DEFAULT_TEMPLATE value is tmux kill-session -t {name}', () => {
|
||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||
assert.ok(
|
||||
source.includes("'tmux kill-session -t {name}'") || source.includes('"tmux kill-session -t {name}"'),
|
||||
"DELETE_SESSION_DEFAULT_TEMPLATE must be set to 'tmux kill-session -t {name}'"
|
||||
);
|
||||
});
|
||||
|
||||
test('openSettings loads delete_session_template from server settings', () => {
|
||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||
const fnStart = source.indexOf('function openSettings(');
|
||||
assert.ok(fnStart !== -1, 'openSettings must exist');
|
||||
const fnEnd = source.indexOf('\nfunction ', fnStart + 1);
|
||||
const fnBody = source.substring(fnStart, fnEnd > fnStart ? fnEnd : fnStart + 3000);
|
||||
assert.ok(
|
||||
fnBody.includes('setting-delete-template'),
|
||||
'openSettings must populate #setting-delete-template from server settings'
|
||||
);
|
||||
});
|
||||
|
||||
test('bindStaticEventListeners wires delete template input to save', () => {
|
||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||
const fnStart = source.indexOf('function bindStaticEventListeners(');
|
||||
assert.ok(fnStart !== -1, 'bindStaticEventListeners must exist');
|
||||
const fnEnd = source.indexOf('\nfunction ', fnStart + 1);
|
||||
const fnBody = source.substring(fnStart, fnEnd > fnStart ? fnEnd : fnStart + 6000);
|
||||
assert.ok(
|
||||
fnBody.includes('setting-delete-template'),
|
||||
'bindStaticEventListeners must wire #setting-delete-template input event to save'
|
||||
);
|
||||
});
|
||||
|
||||
test('bindStaticEventListeners wires delete template reset button', () => {
|
||||
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||
const fnStart = source.indexOf('function bindStaticEventListeners(');
|
||||
assert.ok(fnStart !== -1, 'bindStaticEventListeners must exist');
|
||||
const fnEnd = source.indexOf('\nfunction ', fnStart + 1);
|
||||
const fnBody = source.substring(fnStart, fnEnd > fnStart ? fnEnd : fnStart + 6000);
|
||||
assert.ok(
|
||||
fnBody.includes('setting-delete-template-reset'),
|
||||
'bindStaticEventListeners must wire #setting-delete-template-reset click handler'
|
||||
);
|
||||
});
|
||||
|
||||
+28
-5
@@ -411,9 +411,13 @@ async def delete_current_session() -> dict:
|
||||
|
||||
@app.delete("/api/sessions/{name}")
|
||||
async def delete_session(name: str) -> dict:
|
||||
"""Kill a tmux session by name.
|
||||
"""Kill/destroy a tmux session using the delete_session_template from settings.
|
||||
|
||||
Runs `tmux kill-session -t {name}`. Returns {ok: True, name: name}.
|
||||
Reads delete_session_template, substitutes {name}, and runs it synchronously
|
||||
(30s timeout) so the caller can rely on the session being gone on return.
|
||||
|
||||
Returns {ok: True, name: name}. Errors are logged as warnings — the endpoint
|
||||
always returns 200 so the UI can refresh and reflect the gone session.
|
||||
404 if session is not in the known session list (when non-empty).
|
||||
Must be declared after DELETE /api/sessions/current so "current" routes correctly.
|
||||
"""
|
||||
@@ -421,10 +425,29 @@ async def delete_session(name: str) -> dict:
|
||||
if known and name not in known:
|
||||
raise HTTPException(status_code=404, detail=f"Session '{name}' not found")
|
||||
|
||||
settings = load_settings()
|
||||
command = settings.get(
|
||||
"delete_session_template", "tmux kill-session -t {name}"
|
||||
).replace("{name}", name)
|
||||
|
||||
try:
|
||||
await run_tmux("kill-session", "-t", name)
|
||||
except RuntimeError:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to kill session '{name}'")
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
_log.warning(
|
||||
"Delete command failed (rc=%d): %s",
|
||||
result.returncode,
|
||||
result.stderr.strip(),
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
_log.warning("Delete command timed out after 30s: %r", command)
|
||||
except Exception:
|
||||
_log.warning("Delete command failed: %r", command)
|
||||
|
||||
return {"ok": True, "name": name}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ DEFAULT_SETTINGS: dict = {
|
||||
"window_size_largest": False,
|
||||
"auto_open_created": True,
|
||||
"new_session_template": "tmux new-session -d -s {name}",
|
||||
"delete_session_template": "tmux kill-session -t {name}",
|
||||
}
|
||||
|
||||
|
||||
|
||||
+128
-9
@@ -1044,20 +1044,34 @@ def test_delete_session_success(client, monkeypatch):
|
||||
|
||||
|
||||
def test_delete_session_calls_kill_session(client, monkeypatch):
|
||||
"""DELETE /api/sessions/{name} calls tmux kill-session -t {name}."""
|
||||
from unittest.mock import AsyncMock
|
||||
"""DELETE /api/sessions/{name} runs 'tmux kill-session -t {name}' via subprocess (default template)."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["my-session"])
|
||||
mock_run_tmux = AsyncMock(return_value="")
|
||||
monkeypatch.setattr("muxplex.main.run_tmux", mock_run_tmux)
|
||||
|
||||
captured = []
|
||||
|
||||
def mock_run(cmd, **kwargs):
|
||||
captured.append(cmd)
|
||||
result = MagicMock()
|
||||
result.returncode = 0
|
||||
result.stderr = ""
|
||||
return result
|
||||
|
||||
with patch("muxplex.main.subprocess.run", side_effect=mock_run):
|
||||
client.delete("/api/sessions/my-session")
|
||||
|
||||
assert mock_run_tmux.called
|
||||
args = mock_run_tmux.call_args[0]
|
||||
assert args[0] == "kill-session"
|
||||
assert "-t" in args
|
||||
assert "my-session" in args
|
||||
assert len(captured) == 1, "subprocess.run must be called exactly once"
|
||||
executed_cmd = captured[0]
|
||||
assert "kill-session" in executed_cmd, (
|
||||
f"Default command must include 'kill-session', got: {executed_cmd!r}"
|
||||
)
|
||||
assert "-t" in executed_cmd, (
|
||||
f"Default command must include '-t', got: {executed_cmd!r}"
|
||||
)
|
||||
assert "my-session" in executed_cmd, (
|
||||
f"Command must include session name 'my-session', got: {executed_cmd!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_delete_session_not_found(client, monkeypatch):
|
||||
@@ -1125,3 +1139,108 @@ def test_login_page_title_contains_hostname(client):
|
||||
assert hostname in response.text, (
|
||||
f"Expected hostname '{hostname}' in title of login page"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DELETE /api/sessions/{name} — custom template (task: customizable delete command)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_delete_session_uses_template_command(client, monkeypatch, tmp_path):
|
||||
"""DELETE /api/sessions/{name} must execute the delete_session_template from settings.
|
||||
|
||||
The template {name} placeholder must be substituted with the session name.
|
||||
The command must be run synchronously via subprocess.run (not run_tmux).
|
||||
"""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
# Make the session appear to exist so the 404 guard passes
|
||||
monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["myworkspace"])
|
||||
|
||||
# Redirect settings to a temp path so we can write a custom template
|
||||
import muxplex.settings as settings_mod
|
||||
|
||||
fake_settings_path = tmp_path / "settings.json"
|
||||
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", fake_settings_path)
|
||||
|
||||
# Write a custom template
|
||||
import json
|
||||
|
||||
fake_settings_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"delete_session_template": "echo destroy {name}",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Capture subprocess.run calls
|
||||
captured_commands = []
|
||||
|
||||
def mock_run(cmd, **kwargs):
|
||||
captured_commands.append(cmd)
|
||||
result = MagicMock()
|
||||
result.returncode = 0
|
||||
result.stderr = ""
|
||||
return result
|
||||
|
||||
with patch("muxplex.main.subprocess.run", side_effect=mock_run):
|
||||
response = client.delete("/api/sessions/myworkspace")
|
||||
|
||||
assert response.status_code == 200, (
|
||||
f"DELETE /api/sessions/myworkspace must return 200, got {response.status_code}"
|
||||
)
|
||||
data = response.json()
|
||||
assert data.get("ok") is True, f"Response must have ok=True, got: {data}"
|
||||
assert data.get("name") == "myworkspace", (
|
||||
f"Response must have name='myworkspace', got: {data}"
|
||||
)
|
||||
|
||||
# Verify template substitution happened
|
||||
assert len(captured_commands) == 1, (
|
||||
f"subprocess.run must be called exactly once, called {len(captured_commands)} times"
|
||||
)
|
||||
executed_cmd = captured_commands[0]
|
||||
assert "myworkspace" in executed_cmd, (
|
||||
f"Executed command must contain session name 'myworkspace', got: {executed_cmd!r}"
|
||||
)
|
||||
assert "echo destroy" in executed_cmd, (
|
||||
f"Executed command must use the custom template, got: {executed_cmd!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_delete_session_default_template_is_tmux_kill(client, monkeypatch, tmp_path):
|
||||
"""DELETE /api/sessions/{name} uses 'tmux kill-session -t {name}' when no custom template is set."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["mysession"])
|
||||
|
||||
# Redirect settings to empty temp file (no settings file = use defaults)
|
||||
import muxplex.settings as settings_mod
|
||||
|
||||
fake_settings_path = tmp_path / "settings.json"
|
||||
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", fake_settings_path)
|
||||
# Don't write any settings — defaults should be used
|
||||
|
||||
captured_commands = []
|
||||
|
||||
def mock_run(cmd, **kwargs):
|
||||
captured_commands.append(cmd)
|
||||
result = MagicMock()
|
||||
result.returncode = 0
|
||||
result.stderr = ""
|
||||
return result
|
||||
|
||||
with patch("muxplex.main.subprocess.run", side_effect=mock_run):
|
||||
response = client.delete("/api/sessions/mysession")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert len(captured_commands) == 1
|
||||
executed_cmd = captured_commands[0]
|
||||
# Default template substituted
|
||||
assert "mysession" in executed_cmd, (
|
||||
f"Default template must substitute session name, got: {executed_cmd!r}"
|
||||
)
|
||||
assert "kill-session" in executed_cmd, (
|
||||
f"Default template must contain 'kill-session', got: {executed_cmd!r}"
|
||||
)
|
||||
|
||||
@@ -1062,3 +1062,94 @@ def test_html_settings_close_btn_exists() -> None:
|
||||
assert dialog.find(id="settings-close-btn") is not None, (
|
||||
"#settings-close-btn must be a descendant of #settings-dialog"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Delete session template (task: customizable delete command)
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_html_delete_template_textarea_exists() -> None:
|
||||
"""New Session (Commands) panel must contain #setting-delete-template textarea."""
|
||||
soup = _SOUP
|
||||
dialog = soup.find(id="settings-dialog")
|
||||
assert dialog is not None, "Missing #settings-dialog"
|
||||
new_session_panel = dialog.find(
|
||||
class_="settings-panel", attrs={"data-tab": "new-session"}
|
||||
)
|
||||
assert new_session_panel is not None, "Missing new-session settings-panel"
|
||||
textarea = new_session_panel.find("textarea", id="setting-delete-template")
|
||||
assert textarea is not None, (
|
||||
"Missing <textarea id='setting-delete-template'> inside new-session panel"
|
||||
)
|
||||
classes = textarea.get("class") or []
|
||||
assert "settings-textarea" in classes, (
|
||||
f"#setting-delete-template must have class 'settings-textarea', has: {classes}"
|
||||
)
|
||||
|
||||
|
||||
def test_html_delete_template_textarea_placeholder() -> None:
|
||||
"""#setting-delete-template must have placeholder 'tmux kill-session -t {name}'."""
|
||||
soup = _SOUP
|
||||
textarea = soup.find("textarea", id="setting-delete-template")
|
||||
assert textarea is not None, "Missing #setting-delete-template textarea"
|
||||
placeholder = textarea.get("placeholder")
|
||||
assert placeholder == "tmux kill-session -t {name}", (
|
||||
f"#setting-delete-template placeholder must be 'tmux kill-session -t {{name}}', "
|
||||
f"got: {placeholder!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_html_delete_template_textarea_rows() -> None:
|
||||
"""#setting-delete-template textarea must have rows=3."""
|
||||
soup = _SOUP
|
||||
textarea = soup.find("textarea", id="setting-delete-template")
|
||||
assert textarea is not None, "Missing #setting-delete-template textarea"
|
||||
rows = textarea.get("rows")
|
||||
assert rows == "3", f"#setting-delete-template must have rows='3', got: {rows!r}"
|
||||
|
||||
|
||||
def test_html_delete_template_reset_button_exists() -> None:
|
||||
"""New Session (Commands) panel must contain #setting-delete-template-reset button."""
|
||||
soup = _SOUP
|
||||
dialog = soup.find(id="settings-dialog")
|
||||
assert dialog is not None, "Missing #settings-dialog"
|
||||
new_session_panel = dialog.find(
|
||||
class_="settings-panel", attrs={"data-tab": "new-session"}
|
||||
)
|
||||
assert new_session_panel is not None, "Missing new-session settings-panel"
|
||||
reset_btn = new_session_panel.find(id="setting-delete-template-reset")
|
||||
assert reset_btn is not None, (
|
||||
"Missing #setting-delete-template-reset inside new-session panel"
|
||||
)
|
||||
classes = reset_btn.get("class") or []
|
||||
assert "settings-action-btn" in classes, (
|
||||
f"#setting-delete-template-reset must have class 'settings-action-btn', has: {classes}"
|
||||
)
|
||||
|
||||
|
||||
def test_html_commands_tab_label() -> None:
|
||||
"""The new-session tab button must be labeled 'Commands' (not 'New Session')."""
|
||||
soup = _SOUP
|
||||
dialog = soup.find(id="settings-dialog")
|
||||
assert dialog is not None, "Missing #settings-dialog"
|
||||
tabs_container = dialog.find("nav", class_="settings-tabs")
|
||||
assert tabs_container is not None, "Missing nav.settings-tabs"
|
||||
tab_btn = tabs_container.find("button", attrs={"data-tab": "new-session"})
|
||||
assert tab_btn is not None, "Missing tab button with data-tab='new-session'"
|
||||
label = tab_btn.get_text(strip=True)
|
||||
assert label == "Commands", (
|
||||
f"Tab button data-tab='new-session' must be labeled 'Commands', got: {label!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_html_new_session_tab_controls_with_delete() -> None:
|
||||
"""New Session (Commands) tab must contain create template, delete template, and both reset buttons."""
|
||||
soup = _SOUP
|
||||
for id_ in (
|
||||
"setting-template",
|
||||
"setting-template-reset",
|
||||
"setting-delete-template",
|
||||
"setting-delete-template-reset",
|
||||
):
|
||||
assert soup.find(id=id_), f"Missing element with id='{id_}'"
|
||||
|
||||
@@ -142,3 +142,45 @@ def test_load_propagates_non_json_errors(monkeypatch):
|
||||
|
||||
with pytest.raises(PermissionError):
|
||||
load_settings()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Delete session template (task: customizable delete command)
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_default_settings_include_delete_template():
|
||||
"""DEFAULT_SETTINGS must include delete_session_template with default tmux kill-session value."""
|
||||
assert "delete_session_template" in DEFAULT_SETTINGS, (
|
||||
"DEFAULT_SETTINGS must include 'delete_session_template'"
|
||||
)
|
||||
assert (
|
||||
DEFAULT_SETTINGS["delete_session_template"] == "tmux kill-session -t {name}"
|
||||
), (
|
||||
f"delete_session_template default must be 'tmux kill-session -t {{name}}', "
|
||||
f"got: {DEFAULT_SETTINGS['delete_session_template']!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_delete_session_template_returned_by_load_settings():
|
||||
"""load_settings() must return delete_session_template with default value."""
|
||||
result = load_settings()
|
||||
assert "delete_session_template" in result, (
|
||||
"load_settings() must include 'delete_session_template'"
|
||||
)
|
||||
assert result["delete_session_template"] == "tmux kill-session -t {name}", (
|
||||
f"load_settings() delete_session_template must default to 'tmux kill-session -t {{name}}', "
|
||||
f"got: {result['delete_session_template']!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_delete_session_template_patchable():
|
||||
"""patch_settings() must accept and persist delete_session_template."""
|
||||
custom = "amplifier-dev ~/dev/{name} --destroy"
|
||||
result = patch_settings({"delete_session_template": custom})
|
||||
assert result["delete_session_template"] == custom, (
|
||||
f"patch_settings() must accept delete_session_template, got: {result['delete_session_template']!r}"
|
||||
)
|
||||
# Verify it was persisted
|
||||
loaded = load_settings()
|
||||
assert loaded["delete_session_template"] == custom
|
||||
|
||||
Reference in New Issue
Block a user