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", () => {

Reply via email to