scriptgini 1.2.1__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {scriptgini-1.2.1 → scriptgini-1.3.0}/PKG-INFO +4 -1
  2. {scriptgini-1.2.1 → scriptgini-1.3.0}/README.md +3 -0
  3. scriptgini-1.3.0/app/__init__.py +3 -0
  4. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/main.py +2 -1
  5. scriptgini-1.3.0/app/models/execution_job.py +68 -0
  6. scriptgini-1.3.0/app/routers/execution.py +165 -0
  7. scriptgini-1.3.0/app/schemas/execution.py +31 -0
  8. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/tasks.py +53 -5
  9. {scriptgini-1.2.1 → scriptgini-1.3.0}/pyproject.toml +1 -1
  10. {scriptgini-1.2.1 → scriptgini-1.3.0}/scriptgini.egg-info/PKG-INFO +4 -1
  11. {scriptgini-1.2.1 → scriptgini-1.3.0}/scriptgini.egg-info/SOURCES.txt +5 -1
  12. scriptgini-1.3.0/tests/test_sprint3_execution.py +659 -0
  13. scriptgini-1.2.1/app/__init__.py +0 -3
  14. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/agents/__init__.py +0 -0
  15. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/agents/prompts.py +0 -0
  16. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/agents/script_gini_agent.py +0 -0
  17. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/cache.py +0 -0
  18. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/celery_app.py +0 -0
  19. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/config.py +0 -0
  20. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/database.py +0 -0
  21. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/llm/__init__.py +0 -0
  22. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/llm/provider.py +0 -0
  23. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/models/__init__.py +0 -0
  24. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/models/api_key.py +0 -0
  25. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/models/bulk_job.py +0 -0
  26. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/models/generated_script.py +0 -0
  27. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/models/membership.py +0 -0
  28. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/models/organization.py +0 -0
  29. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/models/project.py +0 -0
  30. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/models/script_run.py +0 -0
  31. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/models/test_case.py +0 -0
  32. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/models/user.py +0 -0
  33. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/routers/__init__.py +0 -0
  34. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/routers/analytics.py +0 -0
  35. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/routers/api_key.py +0 -0
  36. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/routers/auth.py +0 -0
  37. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/routers/bulk_jobs.py +0 -0
  38. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/routers/demo.py +0 -0
  39. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/routers/organizations.py +0 -0
  40. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/routers/projects.py +0 -0
  41. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/routers/scripts.py +0 -0
  42. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/routers/test_cases.py +0 -0
  43. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/schemas/__init__.py +0 -0
  44. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/schemas/analytics.py +0 -0
  45. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/schemas/api_key.py +0 -0
  46. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/schemas/auth.py +0 -0
  47. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/schemas/bulk_job.py +0 -0
  48. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/schemas/generated_script.py +0 -0
  49. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/schemas/membership.py +0 -0
  50. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/schemas/organization.py +0 -0
  51. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/schemas/project.py +0 -0
  52. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/schemas/test_case.py +0 -0
  53. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/services/api_key.py +0 -0
  54. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/services/auth.py +0 -0
  55. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/services/auth_dependencies.py +0 -0
  56. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/services/git_export.py +0 -0
  57. {scriptgini-1.2.1 → scriptgini-1.3.0}/app/services/rbac.py +0 -0
  58. {scriptgini-1.2.1 → scriptgini-1.3.0}/scriptgini.egg-info/dependency_links.txt +0 -0
  59. {scriptgini-1.2.1 → scriptgini-1.3.0}/scriptgini.egg-info/top_level.txt +0 -0
  60. {scriptgini-1.2.1 → scriptgini-1.3.0}/setup.cfg +0 -0
  61. {scriptgini-1.2.1 → scriptgini-1.3.0}/tests/test_api.py +0 -0
  62. {scriptgini-1.2.1 → scriptgini-1.3.0}/tests/test_auth.py +0 -0
  63. {scriptgini-1.2.1 → scriptgini-1.3.0}/tests/test_coverage.py +0 -0
  64. {scriptgini-1.2.1 → scriptgini-1.3.0}/tests/test_infra_services_coverage.py +0 -0
  65. {scriptgini-1.2.1 → scriptgini-1.3.0}/tests/test_sprint2_rbac.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scriptgini
3
- Version: 1.2.1
3
+ Version: 1.3.0
4
4
  Summary: Agentic AI system that converts functional test cases into automation test scripts.
5
5
  Author: ScriptGini Team
6
6
  License: Proprietary
@@ -16,6 +16,8 @@ Description-Content-Type: text/markdown
16
16
 
17
17
  > **Enterprise-grade Agentic AI system that converts functional test cases into high-quality, review-ready automation test scripts.**
18
18
 
19
+ Current release: v1.3.0 (Sprint 3)
20
+
19
21
  ---
20
22
 
21
23
  ## What is ScriptGini?
@@ -33,6 +35,7 @@ ScriptGini is an AI-powered test automation engine built for Quality Engineering
33
35
  - **Project & AUT management** — Store multiple projects, each with its own base URL and defaults
34
36
  - **Full test case history** — Every generated script is stored in SQLite with status and token usage
35
37
  - **Execution history persistence** — Every run is stored in `script_runs` with stdout/stderr, exit code, and duration
38
+ - **Durable execution APIs** — Async execution jobs with run/status/abort endpoints and idempotency-key support
36
39
  - **Hardened execution sandbox** — Script runs use isolated Python mode, static safety validation, and restricted environment variables
37
40
  - **Bulk job orchestration** — Project-level bulk generate and bulk run with pollable job status
38
41
  - **Run analytics dashboard** — Project-level pass/fail/timeout metrics and recent failure feed
@@ -2,6 +2,8 @@
2
2
 
3
3
  > **Enterprise-grade Agentic AI system that converts functional test cases into high-quality, review-ready automation test scripts.**
4
4
 
5
+ Current release: v1.3.0 (Sprint 3)
6
+
5
7
  ---
6
8
 
7
9
  ## What is ScriptGini?
@@ -19,6 +21,7 @@ ScriptGini is an AI-powered test automation engine built for Quality Engineering
19
21
  - **Project & AUT management** — Store multiple projects, each with its own base URL and defaults
20
22
  - **Full test case history** — Every generated script is stored in SQLite with status and token usage
21
23
  - **Execution history persistence** — Every run is stored in `script_runs` with stdout/stderr, exit code, and duration
24
+ - **Durable execution APIs** — Async execution jobs with run/status/abort endpoints and idempotency-key support
22
25
  - **Hardened execution sandbox** — Script runs use isolated Python mode, static safety validation, and restricted environment variables
23
26
  - **Bulk job orchestration** — Project-level bulk generate and bulk run with pollable job status
24
27
  - **Run analytics dashboard** — Project-level pass/fail/timeout metrics and recent failure feed
@@ -0,0 +1,3 @@
1
+ __version__ = "1.3.0"
2
+ __api_version__ = "v1.3.0"
3
+
@@ -10,7 +10,7 @@ from fastapi.staticfiles import StaticFiles
10
10
  from app import __version__
11
11
  from app.config import settings
12
12
  from app.llm.provider import get_llm_diagnostics
13
- from app.routers import projects, test_cases, scripts, bulk_jobs, analytics, demo, auth, api_key, organizations
13
+ from app.routers import projects, test_cases, scripts, bulk_jobs, analytics, demo, auth, api_key, organizations, execution
14
14
 
15
15
  logging.basicConfig(level=logging.DEBUG if settings.DEBUG else logging.INFO)
16
16
  logger = logging.getLogger(__name__)
@@ -58,6 +58,7 @@ app.include_router(demo.router, prefix="/api/v1")
58
58
  app.include_router(auth.router, prefix="/api/v1")
59
59
  app.include_router(api_key.router, prefix="/api/v1")
60
60
  app.include_router(organizations.router, prefix="/api/v1")
61
+ app.include_router(execution.router, prefix="/api/v1")
61
62
  app.mount("/static", StaticFiles(directory=static_dir), name="static")
62
63
 
63
64
 
@@ -0,0 +1,68 @@
1
+ import enum
2
+ from datetime import datetime, timezone
3
+
4
+ from sqlalchemy import DateTime, Enum as SAEnum, ForeignKey, Integer, JSON, String, Text
5
+ from sqlalchemy.orm import Mapped, mapped_column
6
+
7
+ from app.database import Base
8
+
9
+
10
+ class ExecutionJobStatus(str, enum.Enum):
11
+ pending = "pending"
12
+ running = "running"
13
+ completed = "completed"
14
+ failed = "failed"
15
+ cancelled = "cancelled"
16
+
17
+
18
+ _ALLOWED_TRANSITIONS: dict[ExecutionJobStatus, set[ExecutionJobStatus]] = {
19
+ ExecutionJobStatus.pending: {
20
+ ExecutionJobStatus.running,
21
+ ExecutionJobStatus.completed,
22
+ ExecutionJobStatus.failed,
23
+ ExecutionJobStatus.cancelled,
24
+ },
25
+ ExecutionJobStatus.running: {
26
+ ExecutionJobStatus.completed,
27
+ ExecutionJobStatus.failed,
28
+ ExecutionJobStatus.cancelled,
29
+ },
30
+ ExecutionJobStatus.completed: set(),
31
+ ExecutionJobStatus.failed: set(),
32
+ ExecutionJobStatus.cancelled: set(),
33
+ }
34
+
35
+
36
+ class ExecutionJob(Base):
37
+ __tablename__ = "execution_jobs"
38
+
39
+ id: Mapped[int] = mapped_column(primary_key=True, index=True)
40
+ project_id: Mapped[int] = mapped_column(ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
41
+ script_id: Mapped[int] = mapped_column(ForeignKey("generated_scripts.id", ondelete="CASCADE"), nullable=False, index=True)
42
+ test_case_id: Mapped[int] = mapped_column(ForeignKey("test_cases.id", ondelete="CASCADE"), nullable=False, index=True)
43
+ created_by_user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
44
+ status: Mapped[ExecutionJobStatus] = mapped_column(
45
+ SAEnum(ExecutionJobStatus), nullable=False, default=ExecutionJobStatus.pending
46
+ )
47
+ idempotency_key: Mapped[str | None] = mapped_column(String(255), nullable=True, unique=True, index=True)
48
+ celery_task_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
49
+ request_payload: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict)
50
+ result_payload: Mapped[dict | None] = mapped_column(JSON, nullable=True)
51
+ error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
52
+ started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
53
+ completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
54
+ cancelled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
55
+ created_at: Mapped[datetime] = mapped_column(
56
+ DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
57
+ )
58
+ updated_at: Mapped[datetime] = mapped_column(
59
+ DateTime(timezone=True),
60
+ default=lambda: datetime.now(timezone.utc),
61
+ onupdate=lambda: datetime.now(timezone.utc),
62
+ )
63
+
64
+
65
+ def can_transition(current: ExecutionJobStatus, new_status: ExecutionJobStatus) -> bool:
66
+ if current == new_status:
67
+ return True
68
+ return new_status in _ALLOWED_TRANSITIONS[current]
@@ -0,0 +1,165 @@
1
+ from datetime import datetime, timezone
2
+
3
+ from fastapi import APIRouter, Depends, Header, HTTPException, status
4
+ from sqlalchemy.orm import Session
5
+
6
+ from app.celery_app import celery_app
7
+ from app.database import get_db
8
+ from app.models.execution_job import ExecutionJob, ExecutionJobStatus, can_transition
9
+ from app.models.generated_script import GeneratedScript, ScriptStatus
10
+ from app.schemas.execution import ExecutionJobResponse, ExecutionRunRequest
11
+ from app.services import rbac as rbac_service
12
+ from app.services.auth_dependencies import require_auth_with_scopes
13
+ from app.tasks import execute_script
14
+
15
+ router = APIRouter(prefix="/execution", tags=["Execution"])
16
+
17
+
18
+ def _get_job_or_404(job_id: int, db: Session) -> ExecutionJob:
19
+ job = db.query(ExecutionJob).filter(ExecutionJob.id == job_id).first()
20
+ if not job:
21
+ raise HTTPException(status_code=404, detail="Execution job not found")
22
+ return job
23
+
24
+
25
+ def _ensure_project_access(db: Session, project_id: int, user_id: int, require_manager: bool = False) -> None:
26
+ rbac_service.ensure_project_exists(db, project_id)
27
+ existing_members = rbac_service.list_project_members(db, project_id)
28
+ if not existing_members:
29
+ return
30
+
31
+ membership = rbac_service.get_user_project_membership(db, project_id, user_id)
32
+ if membership is None or membership.role not in rbac_service.READ_ROLES:
33
+ raise HTTPException(status_code=403, detail="Insufficient project role")
34
+
35
+ if require_manager and membership.role not in rbac_service.MANAGER_ROLES:
36
+ raise HTTPException(status_code=403, detail="Insufficient project role")
37
+
38
+
39
+ def _apply_status_from_celery(job: ExecutionJob, db: Session) -> None:
40
+ if not job.celery_task_id or job.status not in {ExecutionJobStatus.pending, ExecutionJobStatus.running}:
41
+ return
42
+
43
+ task_result = celery_app.AsyncResult(job.celery_task_id)
44
+ state = task_result.state
45
+
46
+ if state == "PENDING":
47
+ return
48
+
49
+ if state == "STARTED" and can_transition(job.status, ExecutionJobStatus.running):
50
+ job.status = ExecutionJobStatus.running
51
+ if job.started_at is None:
52
+ job.started_at = datetime.now(timezone.utc)
53
+ db.commit()
54
+ db.refresh(job)
55
+ return
56
+
57
+ if state == "SUCCESS" and can_transition(job.status, ExecutionJobStatus.completed):
58
+ job.status = ExecutionJobStatus.completed
59
+ job.completed_at = datetime.now(timezone.utc)
60
+ if job.started_at is None:
61
+ job.started_at = job.completed_at
62
+ result = task_result.result
63
+ job.result_payload = result if isinstance(result, dict) else {"result": str(result)}
64
+ db.commit()
65
+ db.refresh(job)
66
+ return
67
+
68
+ if state == "REVOKED" and can_transition(job.status, ExecutionJobStatus.cancelled):
69
+ job.status = ExecutionJobStatus.cancelled
70
+ job.cancelled_at = datetime.now(timezone.utc)
71
+ db.commit()
72
+ db.refresh(job)
73
+ return
74
+
75
+ if state == "FAILURE" and can_transition(job.status, ExecutionJobStatus.failed):
76
+ job.status = ExecutionJobStatus.failed
77
+ job.completed_at = datetime.now(timezone.utc)
78
+ job.error_message = str(task_result.result)
79
+ db.commit()
80
+ db.refresh(job)
81
+
82
+
83
+ def _enqueue_execution_task(script_id: int, execution_env: dict[str, str], job_id: int) -> str:
84
+ async_result = execute_script.delay(script_id, execution_env, job_id)
85
+ return async_result.id
86
+
87
+
88
+ @router.post("/run", response_model=ExecutionJobResponse, status_code=status.HTTP_202_ACCEPTED)
89
+ def run_execution_job(
90
+ payload: ExecutionRunRequest,
91
+ idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
92
+ current_user=Depends(require_auth_with_scopes({"execution:write"})),
93
+ db: Session = Depends(get_db),
94
+ ):
95
+ script = db.query(GeneratedScript).filter(GeneratedScript.id == payload.script_id).first()
96
+ if not script:
97
+ raise HTTPException(status_code=404, detail="Script not found")
98
+ if script.status != ScriptStatus.completed or not script.script_content:
99
+ raise HTTPException(status_code=400, detail="Script is not ready to run")
100
+
101
+ _ensure_project_access(db, script.project_id, current_user.id)
102
+
103
+ if idempotency_key:
104
+ existing_job = (
105
+ db.query(ExecutionJob)
106
+ .filter(ExecutionJob.idempotency_key == idempotency_key, ExecutionJob.created_by_user_id == current_user.id)
107
+ .first()
108
+ )
109
+ if existing_job:
110
+ _ensure_project_access(db, existing_job.project_id, current_user.id)
111
+ return existing_job
112
+
113
+ job = ExecutionJob(
114
+ project_id=script.project_id,
115
+ script_id=script.id,
116
+ test_case_id=script.test_case_id,
117
+ created_by_user_id=current_user.id,
118
+ status=ExecutionJobStatus.pending,
119
+ idempotency_key=idempotency_key,
120
+ request_payload=payload.model_dump(),
121
+ )
122
+ db.add(job)
123
+ db.commit()
124
+ db.refresh(job)
125
+
126
+ job.celery_task_id = _enqueue_execution_task(script.id, payload.execution_env, job.id)
127
+ db.commit()
128
+ db.refresh(job)
129
+ return job
130
+
131
+
132
+ @router.get("/status/{job_id}", response_model=ExecutionJobResponse)
133
+ def get_execution_job_status(
134
+ job_id: int,
135
+ current_user=Depends(require_auth_with_scopes({"execution:read"})),
136
+ db: Session = Depends(get_db),
137
+ ):
138
+ job = _get_job_or_404(job_id, db)
139
+ _ensure_project_access(db, job.project_id, current_user.id)
140
+ _apply_status_from_celery(job, db)
141
+ return job
142
+
143
+
144
+ @router.post("/abort/{job_id}", response_model=ExecutionJobResponse)
145
+ def abort_execution_job(
146
+ job_id: int,
147
+ current_user=Depends(require_auth_with_scopes({"execution:write"})),
148
+ db: Session = Depends(get_db),
149
+ ):
150
+ job = _get_job_or_404(job_id, db)
151
+ _ensure_project_access(db, job.project_id, current_user.id, require_manager=True)
152
+
153
+ if job.status in {ExecutionJobStatus.completed, ExecutionJobStatus.failed, ExecutionJobStatus.cancelled}:
154
+ return job
155
+
156
+ if job.celery_task_id:
157
+ celery_app.control.revoke(job.celery_task_id, terminate=True)
158
+
159
+ if can_transition(job.status, ExecutionJobStatus.cancelled):
160
+ job.status = ExecutionJobStatus.cancelled
161
+ job.cancelled_at = datetime.now(timezone.utc)
162
+ db.commit()
163
+ db.refresh(job)
164
+
165
+ return job
@@ -0,0 +1,31 @@
1
+ from datetime import datetime
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from app.models.execution_job import ExecutionJobStatus
6
+
7
+
8
+ class ExecutionRunRequest(BaseModel):
9
+ script_id: int = Field(..., gt=0)
10
+ execution_env: dict[str, str] = Field(default_factory=dict)
11
+
12
+
13
+ class ExecutionJobResponse(BaseModel):
14
+ id: int
15
+ project_id: int
16
+ script_id: int
17
+ test_case_id: int
18
+ created_by_user_id: int
19
+ status: ExecutionJobStatus
20
+ idempotency_key: str | None
21
+ celery_task_id: str | None
22
+ request_payload: dict
23
+ result_payload: dict | None
24
+ error_message: str | None
25
+ started_at: datetime | None
26
+ completed_at: datetime | None
27
+ cancelled_at: datetime | None
28
+ created_at: datetime
29
+ updated_at: datetime
30
+
31
+ model_config = {"from_attributes": True}
@@ -5,11 +5,13 @@ These tasks are executed by Celery workers and should not block the API.
5
5
  Examples: Script generation, test execution, file processing, etc.
6
6
  """
7
7
 
8
+ from datetime import datetime, timezone
9
+ import logging
10
+
8
11
  from celery import shared_task
9
- from sqlalchemy.orm import Session
10
- from app.database import SessionLocal
11
12
  from app.config import settings
12
- import logging
13
+ from app.database import SessionLocal
14
+ from app.models.execution_job import ExecutionJob, ExecutionJobStatus, can_transition
13
15
 
14
16
  logger = logging.getLogger(__name__)
15
17
 
@@ -46,8 +48,40 @@ def generate_test_script(self, test_case_id: int, project_id: int):
46
48
  db.close()
47
49
 
48
50
 
51
+ def _sync_execution_job_state(
52
+ db,
53
+ execution_job_id: int | None,
54
+ status: ExecutionJobStatus,
55
+ *,
56
+ result_payload: dict | None = None,
57
+ error_message: str | None = None,
58
+ ):
59
+ if execution_job_id is None:
60
+ return
61
+
62
+ job = db.query(ExecutionJob).filter(ExecutionJob.id == execution_job_id).first()
63
+ if not job or not can_transition(job.status, status):
64
+ return
65
+
66
+ now = datetime.now(timezone.utc)
67
+ job.status = status
68
+ if status == ExecutionJobStatus.running and job.started_at is None:
69
+ job.started_at = now
70
+ if status in {ExecutionJobStatus.completed, ExecutionJobStatus.failed}:
71
+ if job.started_at is None:
72
+ job.started_at = now
73
+ job.completed_at = now
74
+ if status == ExecutionJobStatus.cancelled:
75
+ job.cancelled_at = now
76
+ if result_payload is not None:
77
+ job.result_payload = result_payload
78
+ if error_message is not None:
79
+ job.error_message = error_message
80
+ db.commit()
81
+
82
+
49
83
  @shared_task(bind=True, max_retries=settings.CELERY_MAX_RETRIES)
50
- def execute_script(self, script_id: int, execution_env: dict = None):
84
+ def execute_script(self, script_id: int, execution_env: dict | None = None, execution_job_id: int | None = None):
51
85
  """
52
86
  Async task to execute a test script (Playwright/Selenium).
53
87
 
@@ -62,6 +96,7 @@ def execute_script(self, script_id: int, execution_env: dict = None):
62
96
  try:
63
97
  execution_env = execution_env or {}
64
98
  logger.info(f"Executing script {script_id} with env: {execution_env}")
99
+ _sync_execution_job_state(db, execution_job_id, ExecutionJobStatus.running)
65
100
 
66
101
  # TODO: Implement actual script execution logic
67
102
  # This would:
@@ -71,14 +106,27 @@ def execute_script(self, script_id: int, execution_env: dict = None):
71
106
  # 4. Capture results and logs
72
107
  # 5. Store execution record in database
73
108
 
74
- return {
109
+ result = {
75
110
  "status": "success",
76
111
  "script_id": script_id,
77
112
  "execution_id": None, # Will be populated after execution
78
113
  "duration_seconds": 0,
79
114
  }
115
+ _sync_execution_job_state(
116
+ db,
117
+ execution_job_id,
118
+ ExecutionJobStatus.completed,
119
+ result_payload=result,
120
+ )
121
+ return result
80
122
  except Exception as exc:
81
123
  logger.error(f"Error executing script: {exc}")
124
+ _sync_execution_job_state(
125
+ db,
126
+ execution_job_id,
127
+ ExecutionJobStatus.failed,
128
+ error_message=str(exc),
129
+ )
82
130
  raise self.retry(exc=exc, countdown=5 ** self.request.retries)
83
131
  finally:
84
132
  db.close()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scriptgini"
7
- version = "1.2.1"
7
+ version = "1.3.0"
8
8
  description = "Agentic AI system that converts functional test cases into automation test scripts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scriptgini
3
- Version: 1.2.1
3
+ Version: 1.3.0
4
4
  Summary: Agentic AI system that converts functional test cases into automation test scripts.
5
5
  Author: ScriptGini Team
6
6
  License: Proprietary
@@ -16,6 +16,8 @@ Description-Content-Type: text/markdown
16
16
 
17
17
  > **Enterprise-grade Agentic AI system that converts functional test cases into high-quality, review-ready automation test scripts.**
18
18
 
19
+ Current release: v1.3.0 (Sprint 3)
20
+
19
21
  ---
20
22
 
21
23
  ## What is ScriptGini?
@@ -33,6 +35,7 @@ ScriptGini is an AI-powered test automation engine built for Quality Engineering
33
35
  - **Project & AUT management** — Store multiple projects, each with its own base URL and defaults
34
36
  - **Full test case history** — Every generated script is stored in SQLite with status and token usage
35
37
  - **Execution history persistence** — Every run is stored in `script_runs` with stdout/stderr, exit code, and duration
38
+ - **Durable execution APIs** — Async execution jobs with run/status/abort endpoints and idempotency-key support
36
39
  - **Hardened execution sandbox** — Script runs use isolated Python mode, static safety validation, and restricted environment variables
37
40
  - **Bulk job orchestration** — Project-level bulk generate and bulk run with pollable job status
38
41
  - **Run analytics dashboard** — Project-level pass/fail/timeout metrics and recent failure feed
@@ -15,6 +15,7 @@ app/llm/provider.py
15
15
  app/models/__init__.py
16
16
  app/models/api_key.py
17
17
  app/models/bulk_job.py
18
+ app/models/execution_job.py
18
19
  app/models/generated_script.py
19
20
  app/models/membership.py
20
21
  app/models/organization.py
@@ -28,6 +29,7 @@ app/routers/api_key.py
28
29
  app/routers/auth.py
29
30
  app/routers/bulk_jobs.py
30
31
  app/routers/demo.py
32
+ app/routers/execution.py
31
33
  app/routers/organizations.py
32
34
  app/routers/projects.py
33
35
  app/routers/scripts.py
@@ -37,6 +39,7 @@ app/schemas/analytics.py
37
39
  app/schemas/api_key.py
38
40
  app/schemas/auth.py
39
41
  app/schemas/bulk_job.py
42
+ app/schemas/execution.py
40
43
  app/schemas/generated_script.py
41
44
  app/schemas/membership.py
42
45
  app/schemas/organization.py
@@ -55,4 +58,5 @@ tests/test_api.py
55
58
  tests/test_auth.py
56
59
  tests/test_coverage.py
57
60
  tests/test_infra_services_coverage.py
58
- tests/test_sprint2_rbac.py
61
+ tests/test_sprint2_rbac.py
62
+ tests/test_sprint3_execution.py