From 743a401f9839706d4a26e5957229f5e165c085a7 Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Wed, 6 May 2026 11:22:45 +1200 Subject: [PATCH 1/2] feat(system): add ai-engine show/set commands --- CHANGELOG.md | 6 ++++ README.md | 2 ++ .../skills/datamasque-cli/SKILL.md | 2 +- src/datamasque_cli/commands/system.py | 33 ++++++++++++++++++- tests/commands/test_system.py | 28 ++++++++++++++++ 5 files changed, 69 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4bc2ef..7223529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Added +- `dm system ai-engine show` and `dm system ai-engine set ` — view and + configure the AI Engine URL. + ## v1.2.0 ### Added diff --git a/README.md b/README.md index a7da1f2..07fe69f 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,8 @@ dm system upload-licence ./licence.lic # Upload a licence file dm system logs -o logs.tar.gz # Download application logs dm system admin-install --email admin@co.com # Initial admin setup dm system set-locality AU # Set system locality +dm system ai-engine show # Show the configured AI Engine URL +dm system ai-engine set # Point DataMasque at an AI Engine ``` ## JSON output diff --git a/claude-skills/datamasque-cli/skills/datamasque-cli/SKILL.md b/claude-skills/datamasque-cli/skills/datamasque-cli/SKILL.md index b5041ef..ad70cda 100644 --- a/claude-skills/datamasque-cli/skills/datamasque-cli/SKILL.md +++ b/claude-skills/datamasque-cli/skills/datamasque-cli/SKILL.md @@ -1,6 +1,6 @@ --- name: datamasque-cli -description: Use when the user wants to interact with a DataMasque instance — start masking runs, check run status, list connections or rulesets, manage seeds, manage ruleset libraries, check system health, or any task involving the DataMasque API. Triggers on "mask the data", "start a run", "check the run", "list connections", "list rulesets", "upload a seed", "check DataMasque health", "dm status", "ruleset library", or any request to operate DataMasque programmatically. +description: Use when the user wants to interact with a DataMasque instance — start masking runs, check run status, list connections or rulesets, manage seeds, manage ruleset libraries, check system health, configure the AI Engine, or any task involving the DataMasque API. Triggers on "mask the data", "start a run", "check the run", "list connections", "list rulesets", "upload a seed", "check DataMasque health", "dm status", "ruleset library", "configure the AI Engine", "set the AI Engine URL", or any request to operate DataMasque programmatically. argument-hint: e.g. "start a run with docx_masking on var_input_docx" user-invocable: true --- diff --git a/src/datamasque_cli/commands/system.py b/src/datamasque_cli/commands/system.py index 3be67bd..d0543dc 100644 --- a/src/datamasque_cli/commands/system.py +++ b/src/datamasque_cli/commands/system.py @@ -1,4 +1,4 @@ -"""System-level commands: health, licence, logs, admin-install.""" +"""System administration commands.""" from __future__ import annotations @@ -116,3 +116,34 @@ def set_locality( client = get_client(profile) client.set_locality(locality) print_success(f"Locality set to '{locality}'.") + + +ai_engine_app = typer.Typer(help="Configure the AI Engine.", no_args_is_help=True) +app.add_typer(ai_engine_app, name="ai-engine") + + +@ai_engine_app.command("show") +def ai_engine_show( + profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"), + is_json: bool = typer.Option(False, "--json", help="Output as JSON"), +) -> None: + """Show the configured AI Engine URL.""" + client = get_client(profile) + response = client.make_request("GET", "/api/settings/") + url = response.json().get("dm_ai_engine_url") or None + if should_emit_json(is_json): + print_json({"dm_ai_engine_url": url}) + return + # An empty table cell would look like a rendering bug. + render_output({"dm_ai_engine_url": url or ""}, is_json=False, title="AI Engine") + + +@ai_engine_app.command("set") +def ai_engine_set( + url: str = typer.Argument(help="AI Engine base URL"), + profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"), +) -> None: + """Point DataMasque at an AI Engine.""" + client = get_client(profile) + client.make_request("PATCH", "/api/settings/", data={"dm_ai_engine_url": url}) + print_success(f"AI Engine URL set to '{url}'.") diff --git a/tests/commands/test_system.py b/tests/commands/test_system.py index 25f30b0..7e4054b 100644 --- a/tests/commands/test_system.py +++ b/tests/commands/test_system.py @@ -36,3 +36,31 @@ def test_licence_projects_to_user_facing_fields(mock_get_client: MagicMock, runn assert '"platform_name": "DataMasque"' in result.stdout assert "switchable_license_metadata" not in result.stdout assert "license_source" not in result.stdout + + +@patch(f"{MODULE}.get_client") +def test_ai_engine_show_reads_url_from_settings(mock_get_client: MagicMock, runner: CliRunner) -> None: + client = MagicMock() + mock_get_client.return_value = client + response = MagicMock() + response.json.return_value = {"dm_ai_engine_url": "http://engine.example.com:9021"} + client.make_request.return_value = response + + result = runner.invoke(app, ["system", "ai-engine", "show", "--json"]) + + assert result.exit_code == 0 + client.make_request.assert_called_once_with("GET", "/api/settings/") + assert '"dm_ai_engine_url": "http://engine.example.com:9021"' in result.stdout + + +@patch(f"{MODULE}.get_client") +def test_ai_engine_set_patches_settings_with_url(mock_get_client: MagicMock, runner: CliRunner) -> None: + client = MagicMock() + mock_get_client.return_value = client + + result = runner.invoke(app, ["system", "ai-engine", "set", "http://engine.example.com:9021"]) + + assert result.exit_code == 0 + client.make_request.assert_called_once_with( + "PATCH", "/api/settings/", data={"dm_ai_engine_url": "http://engine.example.com:9021"} + ) From d4c0c9245d5d27c44eff9a3123a38a2cb8f6ab25 Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Wed, 13 May 2026 12:47:48 +1200 Subject: [PATCH 2/2] test(system): parametrise ai-engine show across JSON/table paths --- tests/commands/test_system.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/commands/test_system.py b/tests/commands/test_system.py index 7e4054b..2b855fd 100644 --- a/tests/commands/test_system.py +++ b/tests/commands/test_system.py @@ -3,6 +3,7 @@ from datetime import UTC, datetime from unittest.mock import MagicMock, patch +import pytest from datamasque.client.models.license import LicenseInfo, SwitchableLicenseMetadata from typer.testing import CliRunner @@ -38,19 +39,34 @@ def test_licence_projects_to_user_facing_fields(mock_get_client: MagicMock, runn assert "license_source" not in result.stdout +@pytest.mark.parametrize( + ("extra_args", "settings_url", "expected_output"), + [ + (["--json"], "http://engine.example.com:9021", '"dm_ai_engine_url": "http://engine.example.com:9021"'), + ([], "http://engine.example.com:9021", "http://engine.example.com:9021"), + ([], None, ""), + ([], "", ""), + ], +) @patch(f"{MODULE}.get_client") -def test_ai_engine_show_reads_url_from_settings(mock_get_client: MagicMock, runner: CliRunner) -> None: +def test_ai_engine_show( + mock_get_client: MagicMock, + runner: CliRunner, + extra_args: list[str], + settings_url: str | None, + expected_output: str, +) -> None: client = MagicMock() mock_get_client.return_value = client response = MagicMock() - response.json.return_value = {"dm_ai_engine_url": "http://engine.example.com:9021"} + response.json.return_value = {"dm_ai_engine_url": settings_url} client.make_request.return_value = response - result = runner.invoke(app, ["system", "ai-engine", "show", "--json"]) + result = runner.invoke(app, ["system", "ai-engine", "show", *extra_args]) assert result.exit_code == 0 client.make_request.assert_called_once_with("GET", "/api/settings/") - assert '"dm_ai_engine_url": "http://engine.example.com:9021"' in result.stdout + assert expected_output in result.stdout @patch(f"{MODULE}.get_client")