scriptgini 1.6.1__tar.gz → 1.6.2__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 (75) hide show
  1. {scriptgini-1.6.1 → scriptgini-1.6.2}/PKG-INFO +2 -2
  2. {scriptgini-1.6.1 → scriptgini-1.6.2}/README.md +1 -1
  3. scriptgini-1.6.2/app/__init__.py +3 -0
  4. scriptgini-1.6.2/app/celery_app.py +98 -0
  5. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/tasks.py +53 -1
  6. {scriptgini-1.6.1 → scriptgini-1.6.2}/pyproject.toml +1 -1
  7. {scriptgini-1.6.1 → scriptgini-1.6.2}/scriptgini.egg-info/PKG-INFO +2 -2
  8. {scriptgini-1.6.1 → scriptgini-1.6.2}/scriptgini.egg-info/SOURCES.txt +1 -0
  9. scriptgini-1.6.2/tests/test_api_contracts.py +251 -0
  10. scriptgini-1.6.1/app/__init__.py +0 -3
  11. scriptgini-1.6.1/app/celery_app.py +0 -30
  12. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/agents/__init__.py +0 -0
  13. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/agents/prompts.py +0 -0
  14. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/agents/script_gini_agent.py +0 -0
  15. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/cache.py +0 -0
  16. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/config.py +0 -0
  17. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/database.py +0 -0
  18. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/llm/__init__.py +0 -0
  19. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/llm/provider.py +0 -0
  20. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/main.py +0 -0
  21. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/models/__init__.py +0 -0
  22. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/models/api_key.py +0 -0
  23. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/models/bulk_job.py +0 -0
  24. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/models/execution_job.py +0 -0
  25. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/models/generated_script.py +0 -0
  26. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/models/membership.py +0 -0
  27. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/models/organization.py +0 -0
  28. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/models/project.py +0 -0
  29. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/models/script_revision.py +0 -0
  30. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/models/script_run.py +0 -0
  31. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/models/test_case.py +0 -0
  32. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/models/user.py +0 -0
  33. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/routers/__init__.py +0 -0
  34. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/routers/analytics.py +0 -0
  35. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/routers/api_key.py +0 -0
  36. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/routers/auth.py +0 -0
  37. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/routers/bulk_jobs.py +0 -0
  38. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/routers/demo.py +0 -0
  39. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/routers/execution.py +0 -0
  40. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/routers/organizations.py +0 -0
  41. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/routers/projects.py +0 -0
  42. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/routers/reports.py +0 -0
  43. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/routers/scripts.py +0 -0
  44. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/routers/test_cases.py +0 -0
  45. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/schemas/__init__.py +0 -0
  46. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/schemas/analytics.py +0 -0
  47. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/schemas/api_key.py +0 -0
  48. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/schemas/auth.py +0 -0
  49. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/schemas/bulk_job.py +0 -0
  50. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/schemas/execution.py +0 -0
  51. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/schemas/generated_script.py +0 -0
  52. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/schemas/membership.py +0 -0
  53. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/schemas/organization.py +0 -0
  54. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/schemas/project.py +0 -0
  55. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/schemas/reports.py +0 -0
  56. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/schemas/script_revision.py +0 -0
  57. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/schemas/test_case.py +0 -0
  58. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/services/api_key.py +0 -0
  59. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/services/auth.py +0 -0
  60. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/services/auth_dependencies.py +0 -0
  61. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/services/git_export.py +0 -0
  62. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/services/rate_limit.py +0 -0
  63. {scriptgini-1.6.1 → scriptgini-1.6.2}/app/services/rbac.py +0 -0
  64. {scriptgini-1.6.1 → scriptgini-1.6.2}/scriptgini.egg-info/dependency_links.txt +0 -0
  65. {scriptgini-1.6.1 → scriptgini-1.6.2}/scriptgini.egg-info/top_level.txt +0 -0
  66. {scriptgini-1.6.1 → scriptgini-1.6.2}/setup.cfg +0 -0
  67. {scriptgini-1.6.1 → scriptgini-1.6.2}/tests/test_api.py +0 -0
  68. {scriptgini-1.6.1 → scriptgini-1.6.2}/tests/test_auth.py +0 -0
  69. {scriptgini-1.6.1 → scriptgini-1.6.2}/tests/test_coverage.py +0 -0
  70. {scriptgini-1.6.1 → scriptgini-1.6.2}/tests/test_infra_services_coverage.py +0 -0
  71. {scriptgini-1.6.1 → scriptgini-1.6.2}/tests/test_sprint2_rbac.py +0 -0
  72. {scriptgini-1.6.1 → scriptgini-1.6.2}/tests/test_sprint3_execution.py +0 -0
  73. {scriptgini-1.6.1 → scriptgini-1.6.2}/tests/test_sprint5_reporting_analytics.py +0 -0
  74. {scriptgini-1.6.1 → scriptgini-1.6.2}/tests/test_sprint6_coverage_lifecycle.py +0 -0
  75. {scriptgini-1.6.1 → scriptgini-1.6.2}/tests/test_sprint7_rate_limit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scriptgini
3
- Version: 1.6.1
3
+ Version: 1.6.2
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,7 +16,7 @@ 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.5.4 (Sprint 6 patch - RBAC enforcement hardening with 100% passing tests and 100% statement coverage)
19
+ Current release: v1.6.2 (Patch release - API contract guardrails, Celery test-environment fallback, 273/273 tests passing, 100% statement coverage)
20
20
 
21
21
  ---
22
22
 
@@ -2,7 +2,7 @@
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.5.4 (Sprint 6 patch - RBAC enforcement hardening with 100% passing tests and 100% statement coverage)
5
+ Current release: v1.6.2 (Patch release - API contract guardrails, Celery test-environment fallback, 273/273 tests passing, 100% statement coverage)
6
6
 
7
7
  ---
8
8
 
@@ -0,0 +1,3 @@
1
+ __version__ = "1.6.2"
2
+ __api_version__ = "v1.6.2"
3
+
@@ -0,0 +1,98 @@
1
+ try:
2
+ from celery import Celery
3
+ except ModuleNotFoundError: # pragma: no cover - fallback for test environments without celery installed
4
+ from types import SimpleNamespace
5
+
6
+ class _TaskWrapper:
7
+ def __init__(self, func, *, bind: bool = False, max_retries: int | None = None):
8
+ self._func = func
9
+ self.bind = bind
10
+ self.max_retries = max_retries if max_retries is not None else 0
11
+
12
+ def _self_proxy(self):
13
+ return SimpleNamespace(
14
+ request=SimpleNamespace(retries=0),
15
+ max_retries=self.max_retries,
16
+ retry=self._retry,
17
+ )
18
+
19
+ def _retry(self, exc=None, **kwargs):
20
+ if exc is not None:
21
+ raise exc
22
+ raise RuntimeError("Celery retry requested")
23
+
24
+ def run(self, *args, **kwargs):
25
+ if self.bind:
26
+ return self._func(self._self_proxy(), *args, **kwargs)
27
+ return self._func(*args, **kwargs)
28
+
29
+ def delay(self, *args, **kwargs):
30
+ return SimpleNamespace(id="stub-task", result=self.run(*args, **kwargs))
31
+
32
+ def apply(self, args=None, kwargs=None):
33
+ args = args or ()
34
+ kwargs = kwargs or {}
35
+ return SimpleNamespace(result=self.run(*args, **kwargs))
36
+
37
+ def __call__(self, *args, **kwargs):
38
+ return self.run(*args, **kwargs)
39
+
40
+ class Celery: # type: ignore[override]
41
+ def __init__(self, *args, **kwargs):
42
+ self.conf = self._Conf()
43
+ self.control = SimpleNamespace(revoke=lambda *a, **k: None)
44
+ self.AsyncResult = lambda task_id: SimpleNamespace(id=task_id, state="PENDING", result=None, info=None)
45
+
46
+ class _Conf:
47
+ def update(self, *args, **kwargs):
48
+ for mapping in args:
49
+ if isinstance(mapping, dict):
50
+ for key, value in mapping.items():
51
+ setattr(self, key, value)
52
+ for key, value in kwargs.items():
53
+ setattr(self, key, value)
54
+ return None
55
+
56
+ def task(self, *decorator_args, **decorator_kwargs):
57
+ def decorator(func):
58
+ wrapper = _TaskWrapper(
59
+ func,
60
+ bind=bool(decorator_kwargs.get("bind", False)),
61
+ max_retries=decorator_kwargs.get("max_retries"),
62
+ )
63
+ wrapper.__name__ = getattr(func, "__name__", "task")
64
+ wrapper.__doc__ = getattr(func, "__doc__", None)
65
+ return wrapper
66
+
67
+ if decorator_args and callable(decorator_args[0]) and len(decorator_args) == 1 and not decorator_kwargs:
68
+ return decorator(decorator_args[0])
69
+ return decorator
70
+ from app.config import settings
71
+
72
+ # Initialize Celery app
73
+ celery_app = Celery(
74
+ settings.APP_NAME,
75
+ broker=settings.CELERY_BROKER_URL,
76
+ backend=settings.CELERY_RESULT_BACKEND,
77
+ )
78
+
79
+ # Configure Celery
80
+ celery_app.conf.update(
81
+ task_serializer="json",
82
+ accept_content=["json"],
83
+ result_serializer="json",
84
+ timezone="UTC",
85
+ enable_utc=True,
86
+ task_track_started=True,
87
+ task_time_limit=settings.CELERY_TASK_HARD_TIMEOUT,
88
+ task_soft_time_limit=settings.CELERY_TASK_TIMEOUT,
89
+ worker_prefetch_multiplier=4,
90
+ worker_max_tasks_per_child=1000,
91
+ )
92
+
93
+
94
+ @celery_app.task(bind=True, max_retries=settings.CELERY_MAX_RETRIES)
95
+ def debug_task(self):
96
+ """Test task to verify Celery is working."""
97
+ print(f"Request: {self.request!r}")
98
+ return "Celery is working!"
@@ -8,7 +8,59 @@ Examples: Script generation, test execution, file processing, etc.
8
8
  from datetime import datetime, timedelta, timezone
9
9
  import logging
10
10
 
11
- from celery import shared_task
11
+ try:
12
+ from celery import shared_task
13
+ except ModuleNotFoundError: # pragma: no cover - fallback for test environments without celery installed
14
+ from types import SimpleNamespace
15
+
16
+ class _TaskWrapper:
17
+ def __init__(self, func, *, bind: bool = False, max_retries: int | None = None):
18
+ self._func = func
19
+ self.bind = bind
20
+ self.max_retries = max_retries if max_retries is not None else 0
21
+
22
+ def _self_proxy(self):
23
+ return SimpleNamespace(
24
+ request=SimpleNamespace(retries=0),
25
+ max_retries=self.max_retries,
26
+ retry=self._retry,
27
+ )
28
+
29
+ def _retry(self, exc=None, **kwargs):
30
+ if exc is not None:
31
+ raise exc
32
+ raise RuntimeError("Celery retry requested")
33
+
34
+ def run(self, *args, **kwargs):
35
+ if self.bind:
36
+ return self._func(self._self_proxy(), *args, **kwargs)
37
+ return self._func(*args, **kwargs)
38
+
39
+ def delay(self, *args, **kwargs):
40
+ return SimpleNamespace(id="stub-task", result=self.run(*args, **kwargs))
41
+
42
+ def apply(self, args=None, kwargs=None):
43
+ args = args or ()
44
+ kwargs = kwargs or {}
45
+ return SimpleNamespace(result=self.run(*args, **kwargs))
46
+
47
+ def __call__(self, *args, **kwargs):
48
+ return self.run(*args, **kwargs)
49
+
50
+ def shared_task(*decorator_args, **decorator_kwargs):
51
+ def decorator(func):
52
+ wrapper = _TaskWrapper(
53
+ func,
54
+ bind=bool(decorator_kwargs.get("bind", False)),
55
+ max_retries=decorator_kwargs.get("max_retries"),
56
+ )
57
+ wrapper.__name__ = getattr(func, "__name__", "task")
58
+ wrapper.__doc__ = getattr(func, "__doc__", None)
59
+ return wrapper
60
+
61
+ if decorator_args and callable(decorator_args[0]) and len(decorator_args) == 1 and not decorator_kwargs:
62
+ return decorator(decorator_args[0])
63
+ return decorator
12
64
  from app.config import settings
13
65
  from app.database import SessionLocal
14
66
  from app.models.execution_job import ExecutionJob, ExecutionJobStatus, can_transition
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scriptgini"
7
- version = "1.6.1"
7
+ version = "1.6.2"
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.6.1
3
+ Version: 1.6.2
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,7 +16,7 @@ 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.5.4 (Sprint 6 patch - RBAC enforcement hardening with 100% passing tests and 100% statement coverage)
19
+ Current release: v1.6.2 (Patch release - API contract guardrails, Celery test-environment fallback, 273/273 tests passing, 100% statement coverage)
20
20
 
21
21
  ---
22
22
 
@@ -60,6 +60,7 @@ scriptgini.egg-info/SOURCES.txt
60
60
  scriptgini.egg-info/dependency_links.txt
61
61
  scriptgini.egg-info/top_level.txt
62
62
  tests/test_api.py
63
+ tests/test_api_contracts.py
63
64
  tests/test_auth.py
64
65
  tests/test_coverage.py
65
66
  tests/test_infra_services_coverage.py
@@ -0,0 +1,251 @@
1
+ """Contract tests for the stable project, test-case, script, and bulk API surface."""
2
+
3
+ import sys
4
+ import types
5
+
6
+ import pytest
7
+ from fastapi.testclient import TestClient
8
+ from sqlalchemy import create_engine
9
+ from sqlalchemy.orm import sessionmaker
10
+ from sqlalchemy.pool import StaticPool
11
+
12
+
13
+ def _install_celery_stub() -> None:
14
+ if "celery" in sys.modules:
15
+ return
16
+
17
+ celery_module = types.ModuleType("celery")
18
+
19
+ class _Conf:
20
+ def update(self, *args, **kwargs):
21
+ return None
22
+
23
+ class Celery:
24
+ def __init__(self, *args, **kwargs):
25
+ self.conf = _Conf()
26
+
27
+ def task(self, *decorator_args, **decorator_kwargs):
28
+ def decorator(func):
29
+ def delay(*args, **kwargs):
30
+ return None
31
+
32
+ func.delay = delay
33
+ return func
34
+
35
+ if decorator_args and callable(decorator_args[0]) and len(decorator_args) == 1 and not decorator_kwargs:
36
+ return decorator(decorator_args[0])
37
+ return decorator
38
+
39
+ def shared_task(*decorator_args, **decorator_kwargs):
40
+ def decorator(func):
41
+ def delay(*args, **kwargs):
42
+ return None
43
+
44
+ func.delay = delay
45
+ return func
46
+
47
+ if decorator_args and callable(decorator_args[0]) and len(decorator_args) == 1 and not decorator_kwargs:
48
+ return decorator(decorator_args[0])
49
+ return decorator
50
+
51
+ celery_module.Celery = Celery
52
+ celery_module.shared_task = shared_task
53
+ sys.modules["celery"] = celery_module
54
+
55
+
56
+ _install_celery_stub()
57
+
58
+ import app.models.api_key # noqa: F401
59
+ import app.models.bulk_job # noqa: F401
60
+ import app.models.generated_script # noqa: F401
61
+ import app.models.membership # noqa: F401
62
+ import app.models.project # noqa: F401
63
+ import app.models.script_run # noqa: F401
64
+ import app.models.test_case # noqa: F401
65
+ import app.models.user # noqa: F401
66
+
67
+ from app.database import Base, get_db
68
+ from app.main import app as fastapi_app
69
+
70
+
71
+ TEST_DATABASE_URL = "sqlite:///:memory:"
72
+
73
+ engine = create_engine(
74
+ TEST_DATABASE_URL,
75
+ connect_args={"check_same_thread": False},
76
+ poolclass=StaticPool,
77
+ )
78
+ TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
79
+
80
+
81
+ def override_get_db():
82
+ db = TestingSessionLocal()
83
+ try:
84
+ yield db
85
+ finally:
86
+ db.close()
87
+
88
+
89
+ @pytest.fixture(autouse=True)
90
+ def setup_db():
91
+ Base.metadata.create_all(bind=engine)
92
+ fastapi_app.dependency_overrides[get_db] = override_get_db
93
+ try:
94
+ yield
95
+ finally:
96
+ fastapi_app.dependency_overrides.clear()
97
+ Base.metadata.drop_all(bind=engine)
98
+
99
+
100
+ @pytest.fixture()
101
+ def client():
102
+ return TestClient(fastapi_app)
103
+
104
+
105
+ def _register_and_login(client: TestClient, email: str, password: str = "TestPassword123!") -> dict[str, str]:
106
+ register_resp = client.post("/api/v1/auth/register", json={"email": email, "password": password})
107
+ assert register_resp.status_code == 201
108
+
109
+ login_resp = client.post("/api/v1/auth/login", json={"email": email, "password": password})
110
+ assert login_resp.status_code == 200
111
+ return {"Authorization": f"Bearer {login_resp.json()['access_token']}"}
112
+
113
+
114
+ def _create_project(client: TestClient, headers: dict[str, str]) -> dict:
115
+ resp = client.post(
116
+ "/api/v1/projects/",
117
+ headers=headers,
118
+ json={
119
+ "name": "Contract Project",
120
+ "aut_base_url": "https://example.com",
121
+ "default_framework": "playwright_python",
122
+ "selector_preference": "role",
123
+ },
124
+ )
125
+ assert resp.status_code == 201
126
+ return resp.json()
127
+
128
+
129
+ def _create_test_case(client: TestClient, project_id: int, headers: dict[str, str]) -> dict:
130
+ resp = client.post(
131
+ f"/api/v1/projects/{project_id}/test-cases/",
132
+ headers=headers,
133
+ json={
134
+ "title": "Contract TC",
135
+ "format": "step_based",
136
+ "content": "Step 1: open the app",
137
+ },
138
+ )
139
+ assert resp.status_code == 201
140
+ return resp.json()
141
+
142
+
143
+ def test_openapi_documents_stable_project_test_case_script_and_bulk_surface():
144
+ openapi = fastapi_app.openapi()
145
+ paths = openapi["paths"]
146
+
147
+ expected_paths = [
148
+ "/api/v1/projects/",
149
+ "/api/v1/projects/{project_id}",
150
+ "/api/v1/projects/{project_id}/test-cases/",
151
+ "/api/v1/projects/{project_id}/test-cases/{tc_id}/scripts/",
152
+ "/api/v1/projects/{project_id}/test-cases/{tc_id}/scripts/generate",
153
+ "/api/v1/projects/{project_id}/test-cases/{tc_id}/scripts/{script_id}",
154
+ "/api/v1/projects/{project_id}/test-cases/{tc_id}/scripts/{script_id}/runs",
155
+ "/api/v1/projects/{project_id}/test-cases/{tc_id}/scripts/{script_id}/refactor",
156
+ "/api/v1/projects/{project_id}/test-cases/{tc_id}/scripts/{script_id}/version-history",
157
+ "/api/v1/projects/{project_id}/scripts/bulk-generate",
158
+ "/api/v1/projects/{project_id}/scripts/bulk-run",
159
+ "/api/v1/projects/{project_id}/scripts/bulk-jobs/{job_id}",
160
+ ]
161
+
162
+ for path in expected_paths:
163
+ assert path in paths
164
+
165
+ assert paths["/api/v1/projects/"]["get"]["responses"]["200"]
166
+ assert paths["/api/v1/projects/"]["post"]["responses"]["201"]
167
+ assert paths["/api/v1/projects/{project_id}/test-cases/"]["post"]["responses"]["201"]
168
+ assert paths["/api/v1/projects/{project_id}/test-cases/{tc_id}/scripts/generate"]["post"]["responses"]["202"]
169
+ assert paths["/api/v1/projects/{project_id}/scripts/bulk-generate"]["post"]["responses"]["202"]
170
+ assert paths["/api/v1/projects/{project_id}/scripts/bulk-run"]["post"]["responses"]["202"]
171
+
172
+
173
+ def test_stable_project_test_case_script_and_bulk_contract(client: TestClient, monkeypatch):
174
+ headers = _register_and_login(client, "contract-owner@example.com")
175
+ project = _create_project(client, headers)
176
+ test_case = _create_test_case(client, project["id"], headers)
177
+
178
+ monkeypatch.setattr("app.routers.scripts.process_script_generation_job.delay", lambda *args, **kwargs: None)
179
+ monkeypatch.setattr("app.routers.bulk_jobs.process_bulk_generation_job.delay", lambda *args, **kwargs: None)
180
+ monkeypatch.setattr("app.routers.bulk_jobs.process_bulk_execution_job.delay", lambda *args, **kwargs: None)
181
+
182
+ project_list = client.get("/api/v1/projects/")
183
+ assert project_list.status_code == 200
184
+ assert project_list.json()[0]["id"] == project["id"]
185
+ assert project_list.json()[0]["name"] == "Contract Project"
186
+
187
+ tc_list = client.get(f"/api/v1/projects/{project['id']}/test-cases/", headers=headers)
188
+ assert tc_list.status_code == 200
189
+ assert tc_list.json()[0]["id"] == test_case["id"]
190
+ assert tc_list.json()[0]["title"] == "Contract TC"
191
+
192
+ script_generate = client.post(
193
+ f"/api/v1/projects/{project['id']}/test-cases/{test_case['id']}/scripts/generate",
194
+ headers=headers,
195
+ json={"llm_provider": "openai", "framework": "playwright_python"},
196
+ )
197
+ assert script_generate.status_code == 202
198
+ generated = script_generate.json()
199
+ assert generated["project_id"] == project["id"]
200
+ assert generated["test_case_id"] == test_case["id"]
201
+ assert generated["status"] == "pending"
202
+
203
+ script_list = client.get(
204
+ f"/api/v1/projects/{project['id']}/test-cases/{test_case['id']}/scripts/",
205
+ headers=headers,
206
+ )
207
+ assert script_list.status_code == 200
208
+ assert len(script_list.json()) == 1
209
+ assert script_list.json()[0]["id"] == generated["id"]
210
+ assert script_list.json()[0]["status"] == "pending"
211
+
212
+ script_detail = client.get(
213
+ f"/api/v1/projects/{project['id']}/test-cases/{test_case['id']}/scripts/{generated['id']}",
214
+ headers=headers,
215
+ )
216
+ assert script_detail.status_code == 200
217
+ assert script_detail.json()["id"] == generated["id"]
218
+ assert script_detail.json()["project_id"] == project["id"]
219
+
220
+ bulk_generate = client.post(
221
+ f"/api/v1/projects/{project['id']}/scripts/bulk-generate",
222
+ headers=headers,
223
+ json={"test_case_ids": [test_case["id"]], "framework": "playwright_python", "llm_provider": "openai"},
224
+ )
225
+ assert bulk_generate.status_code == 202
226
+ bulk_generate_payload = bulk_generate.json()
227
+ assert bulk_generate_payload["project_id"] == project["id"]
228
+ assert bulk_generate_payload["kind"] == "generate"
229
+ assert bulk_generate_payload["status"] == "pending"
230
+ assert len(bulk_generate_payload["items"]) == 1
231
+ assert bulk_generate_payload["items"][0]["test_case_id"] == test_case["id"]
232
+
233
+ bulk_job = client.get(
234
+ f"/api/v1/projects/{project['id']}/scripts/bulk-jobs/{bulk_generate_payload['id']}",
235
+ headers=headers,
236
+ )
237
+ assert bulk_job.status_code == 200
238
+ assert bulk_job.json()["id"] == bulk_generate_payload["id"]
239
+ assert bulk_job.json()["kind"] == "generate"
240
+
241
+ bulk_run = client.post(
242
+ f"/api/v1/projects/{project['id']}/scripts/bulk-run",
243
+ headers=headers,
244
+ json={"test_case_ids": [test_case["id"]]},
245
+ )
246
+ assert bulk_run.status_code == 202
247
+ bulk_run_payload = bulk_run.json()
248
+ assert bulk_run_payload["project_id"] == project["id"]
249
+ assert bulk_run_payload["kind"] == "run"
250
+ assert bulk_run_payload["status"] == "pending"
251
+ assert bulk_run_payload["items"][0]["test_case_id"] == test_case["id"]
@@ -1,3 +0,0 @@
1
- __version__ = "1.6.1"
2
- __api_version__ = "v1.6.1"
3
-
@@ -1,30 +0,0 @@
1
- from celery import Celery
2
- from app.config import settings
3
-
4
- # Initialize Celery app
5
- celery_app = Celery(
6
- settings.APP_NAME,
7
- broker=settings.CELERY_BROKER_URL,
8
- backend=settings.CELERY_RESULT_BACKEND,
9
- )
10
-
11
- # Configure Celery
12
- celery_app.conf.update(
13
- task_serializer="json",
14
- accept_content=["json"],
15
- result_serializer="json",
16
- timezone="UTC",
17
- enable_utc=True,
18
- task_track_started=True,
19
- task_time_limit=settings.CELERY_TASK_HARD_TIMEOUT,
20
- task_soft_time_limit=settings.CELERY_TASK_TIMEOUT,
21
- worker_prefetch_multiplier=4,
22
- worker_max_tasks_per_child=1000,
23
- )
24
-
25
-
26
- @celery_app.task(bind=True, max_retries=settings.CELERY_MAX_RETRIES)
27
- def debug_task(self):
28
- """Test task to verify Celery is working."""
29
- print(f"Request: {self.request!r}")
30
- return "Celery is working!"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes