This is an automated email from the ASF dual-hosted git repository. yasithdev pushed a commit to branch feat/generic-experiment-launcher in repository https://gitbox.apache.org/repos/asf/airavata-portals.git
commit b14365110c80e53eb9e3f0cebfc88d8fe7f7df8f Author: yasithdev <[email protected]> AuthorDate: Sat Apr 25 01:15:39 2026 -0400 fix(launcher): deferred review polish (deepcopy, type hints, test gaps, aria-labels) --- .../django_airavata/apps/api/launcher_client.py | 19 +++++----- .../apps/api/tests/test_launcher_client.py | 40 ++++++++++++++++++++++ .../js/components/launch/ExperimentMetaHeader.vue | 3 ++ .../tests/unit/stores/launch.spec.ts | 28 +++++++++++++++ 4 files changed, 81 insertions(+), 9 deletions(-) diff --git a/airavata-django-portal/django_airavata/apps/api/launcher_client.py b/airavata-django-portal/django_airavata/apps/api/launcher_client.py index b25ca5e60..514776474 100644 --- a/airavata-django-portal/django_airavata/apps/api/launcher_client.py +++ b/airavata-django-portal/django_airavata/apps/api/launcher_client.py @@ -9,6 +9,7 @@ stub and the real client. Tests should mock at the ``get_client`` boundary. from __future__ import annotations +import copy from typing import Any, Protocol from django.conf import settings @@ -55,7 +56,7 @@ class _StubClient: self.user_token = user_token def list_applications(self, *, category: str | None, search: str | None) -> list[dict[str, Any]]: - results = list(_STUB_APPS) + results = [copy.deepcopy(a) for a in _STUB_APPS] if category: results = [a for a in results if a["category"] == category] if search: @@ -66,7 +67,7 @@ class _StubClient: def get_application(self, *, app_id: str) -> dict[str, Any]: for a in _STUB_APPS: if a["app_id"] == app_id: - return a + return copy.deepcopy(a) raise LookupError(f"unknown app_id {app_id!r}") def get_project_resource_profile(self, *, project_id: str) -> dict[str, Any]: @@ -152,25 +153,25 @@ class _RealClient: def __init__(self, user_token: str) -> None: self.user_token = user_token - def list_applications(self, *, category, search): + def list_applications(self, *, category: str | None, search: str | None) -> list[dict[str, Any]]: raise NotImplementedError("real client requires airavata server new-model RPCs (Task 28)") - def get_application(self, *, app_id): + def get_application(self, *, app_id: str) -> dict[str, Any]: raise NotImplementedError("real client requires airavata server new-model RPCs (Task 28)") - def get_project_resource_profile(self, *, project_id): + def get_project_resource_profile(self, *, project_id: str) -> dict[str, Any]: raise NotImplementedError("real client requires airavata server new-model RPCs (Task 28)") - def list_user_storages(self): + def list_user_storages(self) -> list[dict[str, Any]]: raise NotImplementedError("real client requires airavata server new-model RPCs (Task 28)") - def list_projects(self): + def list_projects(self) -> list[dict[str, Any]]: raise NotImplementedError("real client requires airavata server new-model RPCs (Task 28)") - def generate_preview(self, draft): + def generate_preview(self, draft: dict[str, Any]) -> dict[str, Any]: raise NotImplementedError("real client requires airavata server new-model RPCs (Task 28)") - def launch_experiment(self, draft): + def launch_experiment(self, draft: dict[str, Any]) -> dict[str, Any]: raise NotImplementedError("real client requires airavata server new-model RPCs (Task 28)") diff --git a/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_client.py b/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_client.py index 2ba428000..77426d2a4 100644 --- a/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_client.py +++ b/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_client.py @@ -57,3 +57,43 @@ class StubLauncherClientTest(TestCase): self.assertTrue(len(projects) >= 1) self.assertIn("project_id", projects[0]) self.assertIn("name", projects[0]) + + @override_settings(LAUNCHER_CLIENT_STUB=True) + def test_generate_preview_emits_slurm_script_for_bridges(self): + client = launcher_client.get_client(user_token="ignored") + draft = { + "name": "x", + "project_id": "proj-stub", + "app_id": "app-stub", + "interface_name": "run", + "inputs": {}, + "outputs": {}, + "runtime": { + "compute_resource_id": "bridges-2", + "partition": "RM", + "walltime": "01:00:00", + "nodes": 1, + "cpus_per_node": 8, + }, + } + preview = client.generate_preview(draft) + self.assertTrue(preview["invocation_command"].startswith("sbatch")) + self.assertIn("#SBATCH", preview["script_contents"]) + + @override_settings(LAUNCHER_CLIENT_STUB=True) + def test_list_applications_filters_by_category(self): + client = launcher_client.get_client(user_token="ignored") + results = client.list_applications(category="Molecular Dynamics", search=None) + self.assertTrue(len(results) >= 1) + self.assertTrue(all(a["category"] == "Molecular Dynamics" for a in results)) + + @override_settings(LAUNCHER_CLIENT_STUB=True) + def test_list_applications_filters_by_search(self): + client = launcher_client.get_client(user_token="ignored") + results = client.list_applications(category=None, search="namd") + self.assertTrue(any("NAMD" in a["name"] for a in results)) + + @override_settings(LAUNCHER_CLIENT_STUB=True) + def test_list_applications_search_returns_empty_when_no_match(self): + client = launcher_client.get_client(user_token="ignored") + self.assertEqual(client.list_applications(category=None, search="nonexistent-app"), []) diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ExperimentMetaHeader.vue b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ExperimentMetaHeader.vue index 2e9e030e2..b2300dbd7 100644 --- a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ExperimentMetaHeader.vue +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/launch/ExperimentMetaHeader.vue @@ -3,6 +3,7 @@ <div class="col-md-6"> <input data-test="exp-name" + aria-label="Experiment name" class="form-control" :value="store.draft.name" placeholder="Experiment name" @@ -13,6 +14,7 @@ <div class="col-md-6"> <select data-test="exp-project" + aria-label="Project" class="form-select" :value="store.draft.project_id ?? ''" @change="onProject(($event.target as HTMLSelectElement).value)" @@ -26,6 +28,7 @@ <div class="col-12"> <textarea data-test="exp-description" + aria-label="Description" class="form-control" rows="2" :value="store.draft.description" diff --git a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/stores/launch.spec.ts b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/stores/launch.spec.ts index ad073e075..3b3527233 100644 --- a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/stores/launch.spec.ts +++ b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/stores/launch.spec.ts @@ -63,6 +63,34 @@ describe("useLaunchStore", () => { s.setMeta({ name: "x", project_id: "p1", description: "" }); expect(s.draftHash).toBe(h2); }); + + it("treats non-required null inputs as valid for tab1", () => { + const s = useLaunchStore(); + const iface = { + name: "run", + inputs: [ + { name: "a", type: "int" as const, required: true }, + { name: "b", type: "int" as const, required: false }, + ], + outputs: [], + }; + s.setMeta({ name: "x", project_id: "p1", description: "" }); + s.pickApp({ app_id: "x", name: "X", category: "C", + content: { kind: "github", url: "g" }, interfaces: [iface] }); + s.pickInterface("run"); + s.setInput("a", 1); + // 'b' deliberately unset (null), but optional + expect(s.tab1Valid).toBe(true); + }); + + it("rejects empty walltime in tab2Valid", () => { + const s = useLaunchStore(); + s.setRuntime({ + compute_resource_id: "cr-1", partition: "RM", + walltime: "", nodes: 1, cpus_per_node: 8, + }); + expect(s.tab2Valid).toBe(false); + }); }); describe("setters and reset", () => {
