methodology-framework 0.3.2__tar.gz → 0.4.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.
- {methodology_framework-0.3.2/src/methodology_framework.egg-info → methodology_framework-0.4.0}/PKG-INFO +1 -1
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/pyproject.toml +1 -1
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/jira_discovery.py +13 -5
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/populate_acus.py +29 -12
- {methodology_framework-0.3.2 → methodology_framework-0.4.0/src/methodology_framework.egg-info}/PKG-INFO +1 -1
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework.egg-info/SOURCES.txt +1 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/tests/test_jira_discovery.py +6 -2
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/tests/test_populate_acus.py +99 -2
- methodology_framework-0.4.0/tests/test_populate_acus_integration.py +97 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/LICENSE +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/README.md +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/setup.cfg +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/__init__.py +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/__main__.py +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/bootstrap_jira.py +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/build_playbook.py +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/jira_shapes/__init__.py +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/jira_shapes/automation_rules.yaml +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/jira_shapes/custom_fields.yaml +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/jira_shapes/workflow.yaml +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/playbooks/bindings/meth.yaml +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/playbooks/bindings/scrum.yaml +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/playbooks/scrum-router.body.md +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/register_playbook_with_devin.py +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/specs/devin-story-format.md +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/sync_stories_to_jira.py +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/templates/github_workflows/populate-story-acus-caller.yml +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/templates/story.md +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework.egg-info/dependency_links.txt +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework.egg-info/requires.txt +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework.egg-info/top_level.txt +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/tests/test_bootstrap_jira.py +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/tests/test_build_playbook.py +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/tests/test_register_playbook_with_devin.py +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/tests/test_sync_stories_to_jira.py +0 -0
- {methodology_framework-0.3.2 → methodology_framework-0.4.0}/tests/test_workflows.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "methodology-framework"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Portable process tooling for agent-driven delivery — scripts, playbooks, templates, and specs."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -283,17 +283,25 @@ def discover_issue_types(
|
|
|
283
283
|
base_url: str,
|
|
284
284
|
project_key: str,
|
|
285
285
|
) -> dict[str, str]:
|
|
286
|
-
"""Discover issue type name→ID map
|
|
286
|
+
"""Discover issue type name→ID map for a specific project.
|
|
287
|
+
|
|
288
|
+
Uses GET /rest/api/3/project/{key} which returns the project object
|
|
289
|
+
with its project-scoped ``issueTypes`` array — each project has its
|
|
290
|
+
own per-project issue type IDs, so the global ``/issuetype`` endpoint
|
|
291
|
+
is unsafe (it returns types across all projects and dict-overwrite
|
|
292
|
+
picks the wrong project's ID when names collide).
|
|
287
293
|
|
|
288
294
|
Returns mapping from issue type name to ID string.
|
|
289
|
-
Raises DiscoveryError if "Story" or "Sub-task" is missing
|
|
295
|
+
Raises DiscoveryError if "Story" or "Sub-task" is missing from the
|
|
296
|
+
project's issue type list.
|
|
290
297
|
"""
|
|
291
|
-
resp = session.get(f"{base_url}/rest/api/3/
|
|
298
|
+
resp = session.get(f"{base_url}/rest/api/3/project/{project_key}")
|
|
292
299
|
resp.raise_for_status()
|
|
293
|
-
|
|
300
|
+
project_data: dict[str, Any] = resp.json()
|
|
301
|
+
project_issue_types: list[dict[str, Any]] = project_data.get("issueTypes", [])
|
|
294
302
|
|
|
295
303
|
name_to_id: dict[str, str] = {}
|
|
296
|
-
for it in
|
|
304
|
+
for it in project_issue_types:
|
|
297
305
|
name_to_id[it["name"]] = it["id"]
|
|
298
306
|
|
|
299
307
|
for required_name in (ISSUE_TYPE_NAME_STORY, ISSUE_TYPE_NAME_SUBTASK):
|
|
@@ -77,25 +77,32 @@ def rewrite_acus(content: str, acus_value: float) -> tuple[str, str]:
|
|
|
77
77
|
return content, ""
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def fetch_acus_from_devin(session_id: str, api_token: str) -> float | None:
|
|
80
|
+
def fetch_acus_from_devin(session_id: str, api_token: str, org_id: str) -> float | None:
|
|
81
81
|
"""Fetch ACU consumption from Devin API for a given session ID.
|
|
82
82
|
|
|
83
83
|
Returns the ACU value or None on error.
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
|
|
85
|
+
Devin API v3 (org-scoped, service-user auth):
|
|
86
|
+
GET /v3/organizations/{org_id}/sessions/{devin_id}
|
|
87
|
+
Returns JSON with ``acus_consumed`` (float) at the top level.
|
|
88
|
+
|
|
89
|
+
The session ID extracted from story file URLs (e.g.
|
|
90
|
+
``https://app.devin.ai/sessions/abc123``) is the raw ID without prefix.
|
|
91
|
+
The v3 path parameter expects the ``devin-`` prefix, so this function
|
|
92
|
+
prepends it when constructing the URL.
|
|
86
93
|
"""
|
|
87
|
-
|
|
94
|
+
devin_id = f"devin-{session_id}" if not session_id.startswith("devin-") else session_id
|
|
95
|
+
url = f"https://api.devin.ai/v3/organizations/{org_id}/sessions/{devin_id}"
|
|
88
96
|
headers = {"Authorization": f"Bearer {api_token}"}
|
|
89
97
|
try:
|
|
90
98
|
resp = requests.get(url, headers=headers, timeout=30)
|
|
91
99
|
resp.raise_for_status()
|
|
92
100
|
data = resp.json()
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
acus
|
|
97
|
-
|
|
98
|
-
return float(acus)
|
|
101
|
+
# v3 field name is "acus_consumed"; keep legacy fallbacks for robustness.
|
|
102
|
+
for field in ("acus_consumed", "total_acus", "acus"):
|
|
103
|
+
acus = data.get(field)
|
|
104
|
+
if acus is not None:
|
|
105
|
+
return float(acus)
|
|
99
106
|
logger.warning(
|
|
100
107
|
"Session %s: no ACU field found in response keys: %s",
|
|
101
108
|
session_id,
|
|
@@ -113,6 +120,7 @@ def fetch_acus_from_devin(session_id: str, api_token: str) -> float | None:
|
|
|
113
120
|
def scan_and_populate(
|
|
114
121
|
story_path_pattern: str,
|
|
115
122
|
api_token: str,
|
|
123
|
+
org_id: str,
|
|
116
124
|
*,
|
|
117
125
|
dry_run: bool = False,
|
|
118
126
|
) -> list[PopulateResult]:
|
|
@@ -136,7 +144,7 @@ def scan_and_populate(
|
|
|
136
144
|
logger.info("Skipping %s: no agent_session_url found", filepath)
|
|
137
145
|
continue
|
|
138
146
|
|
|
139
|
-
acus = fetch_acus_from_devin(session_id, api_token)
|
|
147
|
+
acus = fetch_acus_from_devin(session_id, api_token, org_id)
|
|
140
148
|
if acus is None:
|
|
141
149
|
logger.warning(
|
|
142
150
|
"Skipping %s: could not fetch ACUs for session %s",
|
|
@@ -193,7 +201,16 @@ def main() -> int:
|
|
|
193
201
|
logger.error("DEVIN_API_TOKEN environment variable is required")
|
|
194
202
|
return 1
|
|
195
203
|
|
|
196
|
-
|
|
204
|
+
org_id = os.environ.get("DEVIN_ORG_ID", "")
|
|
205
|
+
if not org_id:
|
|
206
|
+
logger.error(
|
|
207
|
+
"DEVIN_ORG_ID environment variable is required "
|
|
208
|
+
"(the Devin organization ID; obtain from your service user setup "
|
|
209
|
+
"or the Devin webapp settings)."
|
|
210
|
+
)
|
|
211
|
+
return 1
|
|
212
|
+
|
|
213
|
+
results = scan_and_populate(args.story_path_pattern, api_token, org_id, dry_run=args.dry_run)
|
|
197
214
|
|
|
198
215
|
if not results:
|
|
199
216
|
print("Nothing to do — all agent_acus fields already populated.")
|
|
@@ -28,6 +28,7 @@ tests/test_bootstrap_jira.py
|
|
|
28
28
|
tests/test_build_playbook.py
|
|
29
29
|
tests/test_jira_discovery.py
|
|
30
30
|
tests/test_populate_acus.py
|
|
31
|
+
tests/test_populate_acus_integration.py
|
|
31
32
|
tests/test_register_playbook_with_devin.py
|
|
32
33
|
tests/test_sync_stories_to_jira.py
|
|
33
34
|
tests/test_workflows.py
|
|
@@ -111,8 +111,12 @@ def _mock_session(
|
|
|
111
111
|
resp.json.return_value = statuses or STATUSES_RESPONSE
|
|
112
112
|
elif url.endswith("/issueLinkType"):
|
|
113
113
|
resp.json.return_value = link_types or LINK_TYPES_RESPONSE
|
|
114
|
-
elif
|
|
115
|
-
|
|
114
|
+
elif "/project/" in url:
|
|
115
|
+
# Project endpoint returns the project object with embedded issueTypes.
|
|
116
|
+
# (v0.3.3 switched discover_issue_types from the global /issuetype
|
|
117
|
+
# endpoint to the project-scoped /project/{key} endpoint to fix
|
|
118
|
+
# cross-project ID collisions.)
|
|
119
|
+
resp.json.return_value = {"issueTypes": issue_types or ISSUE_TYPES_RESPONSE}
|
|
116
120
|
return resp
|
|
117
121
|
|
|
118
122
|
session.get = _get
|
|
@@ -135,7 +135,7 @@ def test_devin_api_error_skips_file() -> None:
|
|
|
135
135
|
mock_resp.status_code = 404
|
|
136
136
|
mock_resp.raise_for_status.side_effect = requests.HTTPError(response=mock_resp)
|
|
137
137
|
|
|
138
|
-
result = fetch_acus_from_devin("nonexistent-session", "fake-token")
|
|
138
|
+
result = fetch_acus_from_devin("nonexistent-session", "fake-token", "org-test")
|
|
139
139
|
assert result is None
|
|
140
140
|
|
|
141
141
|
# Also test 401
|
|
@@ -144,5 +144,102 @@ def test_devin_api_error_skips_file() -> None:
|
|
|
144
144
|
mock_resp.status_code = 401
|
|
145
145
|
mock_resp.raise_for_status.side_effect = requests.HTTPError(response=mock_resp)
|
|
146
146
|
|
|
147
|
-
result = fetch_acus_from_devin("bad-auth-session", "fake-token")
|
|
147
|
+
result = fetch_acus_from_devin("bad-auth-session", "fake-token", "org-test")
|
|
148
148
|
assert result is None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# test_v3_url_pattern
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_v3_url_pattern() -> None:
|
|
157
|
+
"""fetch_acus_from_devin constructs the v3 org-scoped URL with devin- prefix."""
|
|
158
|
+
with patch("methodology_framework.populate_acus.requests.get") as mock_get:
|
|
159
|
+
mock_resp = mock_get.return_value
|
|
160
|
+
mock_resp.raise_for_status.return_value = None
|
|
161
|
+
mock_resp.json.return_value = {"acus_consumed": 3.5}
|
|
162
|
+
|
|
163
|
+
result = fetch_acus_from_devin("abc123", "fake-token", "org-myorg")
|
|
164
|
+
assert result == 3.5
|
|
165
|
+
|
|
166
|
+
# Verify the URL used
|
|
167
|
+
call_url = mock_get.call_args[0][0]
|
|
168
|
+
assert call_url == "https://api.devin.ai/v3/organizations/org-myorg/sessions/devin-abc123"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_v3_url_pattern_already_prefixed() -> None:
|
|
172
|
+
"""Session IDs already prefixed with devin- are not double-prefixed."""
|
|
173
|
+
with patch("methodology_framework.populate_acus.requests.get") as mock_get:
|
|
174
|
+
mock_resp = mock_get.return_value
|
|
175
|
+
mock_resp.raise_for_status.return_value = None
|
|
176
|
+
mock_resp.json.return_value = {"acus_consumed": 7.0}
|
|
177
|
+
|
|
178
|
+
result = fetch_acus_from_devin("devin-abc123", "fake-token", "org-myorg")
|
|
179
|
+
assert result == 7.0
|
|
180
|
+
|
|
181
|
+
call_url = mock_get.call_args[0][0]
|
|
182
|
+
assert call_url == "https://api.devin.ai/v3/organizations/org-myorg/sessions/devin-abc123"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_acus_consumed_field_preferred() -> None:
|
|
186
|
+
"""v3 field acus_consumed is preferred over legacy total_acus/acus."""
|
|
187
|
+
with patch("methodology_framework.populate_acus.requests.get") as mock_get:
|
|
188
|
+
mock_resp = mock_get.return_value
|
|
189
|
+
mock_resp.raise_for_status.return_value = None
|
|
190
|
+
mock_resp.json.return_value = {
|
|
191
|
+
"acus_consumed": 5.0,
|
|
192
|
+
"total_acus": 10.0,
|
|
193
|
+
"acus": 15.0,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
result = fetch_acus_from_devin("sess1", "fake-token", "org-test")
|
|
197
|
+
assert result == 5.0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_legacy_total_acus_fallback() -> None:
|
|
201
|
+
"""Falls back to total_acus when acus_consumed is absent."""
|
|
202
|
+
with patch("methodology_framework.populate_acus.requests.get") as mock_get:
|
|
203
|
+
mock_resp = mock_get.return_value
|
|
204
|
+
mock_resp.raise_for_status.return_value = None
|
|
205
|
+
mock_resp.json.return_value = {"total_acus": 10.0}
|
|
206
|
+
|
|
207
|
+
result = fetch_acus_from_devin("sess1", "fake-token", "org-test")
|
|
208
|
+
assert result == 10.0
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_no_acu_field_returns_none() -> None:
|
|
212
|
+
"""Returns None and logs warning when no ACU field is present."""
|
|
213
|
+
with patch("methodology_framework.populate_acus.requests.get") as mock_get:
|
|
214
|
+
mock_resp = mock_get.return_value
|
|
215
|
+
mock_resp.raise_for_status.return_value = None
|
|
216
|
+
mock_resp.json.return_value = {"session_id": "sess1", "status": "exit"}
|
|
217
|
+
|
|
218
|
+
result = fetch_acus_from_devin("sess1", "fake-token", "org-test")
|
|
219
|
+
assert result is None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_main_fails_without_devin_org_id() -> None:
|
|
223
|
+
"""main() exits 1 with clear error when DEVIN_ORG_ID is missing."""
|
|
224
|
+
import os
|
|
225
|
+
import sys
|
|
226
|
+
|
|
227
|
+
from methodology_framework.populate_acus import main
|
|
228
|
+
|
|
229
|
+
env = {"DEVIN_API_TOKEN": "fake-token"}
|
|
230
|
+
with patch.dict(os.environ, env, clear=True), patch.object(sys, "argv", ["populate_acus"]):
|
|
231
|
+
result = main()
|
|
232
|
+
assert result == 1
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_main_fails_without_devin_api_token() -> None:
|
|
236
|
+
"""main() exits 1 when DEVIN_API_TOKEN is missing."""
|
|
237
|
+
import os
|
|
238
|
+
import sys
|
|
239
|
+
|
|
240
|
+
from methodology_framework.populate_acus import main
|
|
241
|
+
|
|
242
|
+
env = {"DEVIN_ORG_ID": "org-test"}
|
|
243
|
+
with patch.dict(os.environ, env, clear=True), patch.object(sys, "argv", ["populate_acus"]):
|
|
244
|
+
result = main()
|
|
245
|
+
assert result == 1
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Integration tests for methodology_framework.populate_acus — contract-level.
|
|
2
|
+
|
|
3
|
+
These tests hit the real Devin v3 API to verify the URL pattern, auth model,
|
|
4
|
+
and response shape. They are gated on CI secrets and skip locally when the
|
|
5
|
+
required environment variables are absent.
|
|
6
|
+
|
|
7
|
+
Required CI secrets / env vars:
|
|
8
|
+
DEVIN_API_TOKEN — service-user ``cog_*`` API key
|
|
9
|
+
DEVIN_ORG_ID — Devin organization ID (``org-…`` prefix)
|
|
10
|
+
DEVIN_TEST_SESSION_ID — a known sentinel session ID (raw, without ``devin-`` prefix)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
import requests
|
|
19
|
+
|
|
20
|
+
from methodology_framework.populate_acus import fetch_acus_from_devin
|
|
21
|
+
|
|
22
|
+
_API_TOKEN = os.environ.get("DEVIN_API_TOKEN", "")
|
|
23
|
+
_ORG_ID = os.environ.get("DEVIN_ORG_ID", "")
|
|
24
|
+
_SESSION_ID = os.environ.get("DEVIN_TEST_SESSION_ID", "")
|
|
25
|
+
|
|
26
|
+
_SKIP_REASON = (
|
|
27
|
+
"Integration test requires DEVIN_API_TOKEN, DEVIN_ORG_ID, "
|
|
28
|
+
"and DEVIN_TEST_SESSION_ID environment variables"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
pytestmark = pytest.mark.integration
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.mark.skipif(
|
|
35
|
+
not (_API_TOKEN and _ORG_ID and _SESSION_ID),
|
|
36
|
+
reason=_SKIP_REASON,
|
|
37
|
+
)
|
|
38
|
+
def test_v3_session_endpoint_returns_200() -> None:
|
|
39
|
+
"""The v3 org-scoped session endpoint returns HTTP 200 for a known session."""
|
|
40
|
+
devin_id = f"devin-{_SESSION_ID}" if not _SESSION_ID.startswith("devin-") else _SESSION_ID
|
|
41
|
+
url = f"https://api.devin.ai/v3/organizations/{_ORG_ID}/sessions/{devin_id}"
|
|
42
|
+
headers = {"Authorization": f"Bearer {_API_TOKEN}"}
|
|
43
|
+
|
|
44
|
+
resp = requests.get(url, headers=headers, timeout=30)
|
|
45
|
+
|
|
46
|
+
assert resp.status_code == 200, (
|
|
47
|
+
f"Expected 200 from {url}, got {resp.status_code}: {resp.text[:200]}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.skipif(
|
|
52
|
+
not (_API_TOKEN and _ORG_ID and _SESSION_ID),
|
|
53
|
+
reason=_SKIP_REASON,
|
|
54
|
+
)
|
|
55
|
+
def test_v3_response_contains_acus_consumed() -> None:
|
|
56
|
+
"""The v3 session response contains ``acus_consumed`` as a numeric value."""
|
|
57
|
+
devin_id = f"devin-{_SESSION_ID}" if not _SESSION_ID.startswith("devin-") else _SESSION_ID
|
|
58
|
+
url = f"https://api.devin.ai/v3/organizations/{_ORG_ID}/sessions/{devin_id}"
|
|
59
|
+
headers = {"Authorization": f"Bearer {_API_TOKEN}"}
|
|
60
|
+
|
|
61
|
+
resp = requests.get(url, headers=headers, timeout=30)
|
|
62
|
+
resp.raise_for_status()
|
|
63
|
+
data = resp.json()
|
|
64
|
+
|
|
65
|
+
assert "acus_consumed" in data, (
|
|
66
|
+
f"Expected 'acus_consumed' in response keys, got: {sorted(data.keys())}"
|
|
67
|
+
)
|
|
68
|
+
assert isinstance(data["acus_consumed"], (int, float)), (
|
|
69
|
+
f"Expected acus_consumed to be numeric, got {type(data['acus_consumed'])}: "
|
|
70
|
+
f"{data['acus_consumed']}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.skipif(
|
|
75
|
+
not (_API_TOKEN and _ORG_ID and _SESSION_ID),
|
|
76
|
+
reason=_SKIP_REASON,
|
|
77
|
+
)
|
|
78
|
+
def test_fetch_acus_from_devin_returns_float() -> None:
|
|
79
|
+
"""The ``fetch_acus_from_devin()`` helper returns a float for a known session."""
|
|
80
|
+
result = fetch_acus_from_devin(_SESSION_ID, _API_TOKEN, _ORG_ID)
|
|
81
|
+
|
|
82
|
+
assert result is not None, f"fetch_acus_from_devin returned None for session {_SESSION_ID}"
|
|
83
|
+
assert isinstance(result, float), f"Expected float, got {type(result)}: {result}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.mark.skipif(
|
|
87
|
+
not (_API_TOKEN and _ORG_ID and _SESSION_ID),
|
|
88
|
+
reason=_SKIP_REASON,
|
|
89
|
+
)
|
|
90
|
+
def test_url_pattern_matches_v3() -> None:
|
|
91
|
+
"""Verify the URL constructed by fetch_acus_from_devin matches v3 pattern."""
|
|
92
|
+
devin_id = f"devin-{_SESSION_ID}" if not _SESSION_ID.startswith("devin-") else _SESSION_ID
|
|
93
|
+
expected_url = f"https://api.devin.ai/v3/organizations/{_ORG_ID}/sessions/{devin_id}"
|
|
94
|
+
|
|
95
|
+
assert "/v3/organizations/" in expected_url
|
|
96
|
+
assert _ORG_ID in expected_url
|
|
97
|
+
assert devin_id in expected_url
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/__init__.py
RENAMED
|
File without changes
|
{methodology_framework-0.3.2 → methodology_framework-0.4.0}/src/methodology_framework/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{methodology_framework-0.3.2 → methodology_framework-0.4.0}/tests/test_sync_stories_to_jira.py
RENAMED
|
File without changes
|
|
File without changes
|