agentic-devtools 0.2.552__py3-none-any.whl → 0.2.554__py3-none-any.whl
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.
- agentic_devtools/_version.py +2 -2
- agentic_devtools/cli/checks/smart_module_tests.py +180 -0
- agentic_devtools/cli/checks/xdist_policy.py +4 -2
- agentic_devtools/cli/ci/github_provider.py +435 -51
- agentic_devtools/cli/ci/guards.py +250 -3
- agentic_devtools/cli/ci/models.py +9 -0
- agentic_devtools/cli/ci/pipeline/actions/__init__.py +2 -0
- agentic_devtools/cli/ci/pipeline/actions/guards.py +13 -8
- agentic_devtools/cli/ci/pipeline/actions/publish.py +79 -23
- agentic_devtools/cli/ci/pipeline/actions/retry_failed_session.py +515 -0
- agentic_devtools/cli/ci/pipeline/command.py +9 -1
- agentic_devtools/cli/ci/pipeline/runner.py +21 -3
- agentic_devtools/cli/ci/pipeline/session_detector.py +693 -13
- agentic_devtools/cli/ci/pipeline/title_utils.py +19 -0
- agentic_devtools/cli/ci/provider.py +33 -0
- agentic_devtools/cli/ci/scheduler.py +117 -10
- agentic_devtools/cli/ci/scheduler_command.py +9 -0
- agentic_devtools/cli/ci/watchdog_command.py +60 -84
- {agentic_devtools-0.2.552.dist-info → agentic_devtools-0.2.554.dist-info}/METADATA +1 -1
- {agentic_devtools-0.2.552.dist-info → agentic_devtools-0.2.554.dist-info}/RECORD +23 -20
- {agentic_devtools-0.2.552.dist-info → agentic_devtools-0.2.554.dist-info}/WHEEL +0 -0
- {agentic_devtools-0.2.552.dist-info → agentic_devtools-0.2.554.dist-info}/entry_points.txt +0 -0
- {agentic_devtools-0.2.552.dist-info → agentic_devtools-0.2.554.dist-info}/licenses/LICENSE +0 -0
agentic_devtools/_version.py
CHANGED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.554'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 554)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Run the selected PR smart-module test suites with combined coverage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from agentic_devtools.cli.checks.xdist_policy import get_xdist_args
|
|
13
|
+
|
|
14
|
+
_MODULE_FLAGS_ENV = "AGDT_SMART_MODULE_FLAGS"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class _ModuleTestSpec:
|
|
19
|
+
"""Test paths and coverage packages for one smart-module filter."""
|
|
20
|
+
|
|
21
|
+
paths: tuple[str, ...]
|
|
22
|
+
globs: tuple[str, ...]
|
|
23
|
+
coverage: tuple[str, ...]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_MODULE_SPECS = {
|
|
27
|
+
"git": _ModuleTestSpec(("tests/unit/cli/git",), ("tests/test_git_*.py",), ("agentic_devtools/cli/git",)),
|
|
28
|
+
"azure_devops": _ModuleTestSpec(
|
|
29
|
+
("tests/unit/cli/azure_devops", "tests/azure_devops"),
|
|
30
|
+
(),
|
|
31
|
+
("agentic_devtools/cli/azure_devops",),
|
|
32
|
+
),
|
|
33
|
+
"azure_context": _ModuleTestSpec(
|
|
34
|
+
("tests/unit/cli/azure_context", "tests/azure_context"),
|
|
35
|
+
(),
|
|
36
|
+
("agentic_devtools/cli/azure_context",),
|
|
37
|
+
),
|
|
38
|
+
"jira": _ModuleTestSpec(("tests/unit/cli/jira",), (), ("agentic_devtools/cli/jira",)),
|
|
39
|
+
"vpn": _ModuleTestSpec(("tests/unit/cli/vpn", "tests/vpn"), (), ("agentic_devtools/cli/vpn",)),
|
|
40
|
+
"network": _ModuleTestSpec(("tests/unit/cli/network", "tests/network"), (), ("agentic_devtools/cli/network",)),
|
|
41
|
+
"release": _ModuleTestSpec(
|
|
42
|
+
("tests/unit/cli/release",), ("tests/test_release_*.py",), ("agentic_devtools/cli/release",)
|
|
43
|
+
),
|
|
44
|
+
"tasks": _ModuleTestSpec(("tests/unit/cli/tasks",), (), ("agentic_devtools/cli/tasks",)),
|
|
45
|
+
"github_module": _ModuleTestSpec(("tests/unit/cli/github",), (), ("agentic_devtools/cli/github",)),
|
|
46
|
+
"copilot": _ModuleTestSpec(("tests/unit/cli/copilot",), (), ("agentic_devtools/cli/copilot",)),
|
|
47
|
+
"speckit": _ModuleTestSpec(("tests/unit/cli/speckit",), (), ("agentic_devtools/cli/speckit",)),
|
|
48
|
+
"azure": _ModuleTestSpec(("tests/unit/cli/azure",), (), ("agentic_devtools/cli/azure",)),
|
|
49
|
+
"issue_template": _ModuleTestSpec(("tests/unit/cli/issue_template",), (), ("agentic_devtools/cli/issue_template",)),
|
|
50
|
+
"hierarchy": _ModuleTestSpec(
|
|
51
|
+
("tests/unit/hierarchy", "tests/unit/cli/hierarchy"),
|
|
52
|
+
(),
|
|
53
|
+
("agentic_devtools/hierarchy", "agentic_devtools/cli/hierarchy"),
|
|
54
|
+
),
|
|
55
|
+
"workflows": _ModuleTestSpec(("tests/unit/cli/workflows",), (), ("agentic_devtools/cli/workflows",)),
|
|
56
|
+
"core": _ModuleTestSpec(
|
|
57
|
+
(
|
|
58
|
+
"tests/unit/state",
|
|
59
|
+
"tests/unit/background_tasks",
|
|
60
|
+
"tests/unit/config",
|
|
61
|
+
"tests/unit/file_locking",
|
|
62
|
+
"tests/unit/task_state",
|
|
63
|
+
"tests/unit/cli/runner",
|
|
64
|
+
"tests/unit/cli/state",
|
|
65
|
+
"tests/unit/cli/subprocess_utils",
|
|
66
|
+
"tests/unit/cli/testing",
|
|
67
|
+
"tests/test_validate_test_structure.py",
|
|
68
|
+
"tests/test_scaffold_tests.py",
|
|
69
|
+
"tests/test_version.py",
|
|
70
|
+
),
|
|
71
|
+
(),
|
|
72
|
+
(
|
|
73
|
+
"agentic_devtools.state",
|
|
74
|
+
"agentic_devtools.background_tasks",
|
|
75
|
+
"agentic_devtools.config",
|
|
76
|
+
"agentic_devtools.file_locking",
|
|
77
|
+
"agentic_devtools.task_state",
|
|
78
|
+
"agentic_devtools.cli.runner",
|
|
79
|
+
"agentic_devtools.cli.state",
|
|
80
|
+
"agentic_devtools.cli.subprocess_utils",
|
|
81
|
+
"agentic_devtools.cli.testing",
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
"orchestration": _ModuleTestSpec(("tests/unit/orchestration",), (), ("agentic_devtools.orchestration",)),
|
|
85
|
+
"tools": _ModuleTestSpec(("tests/unit/tools",), (), ("agentic_devtools.tools",)),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SmartModuleTestRunner:
|
|
90
|
+
"""Select and run module-scoped tests using xdist and combined coverage."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, selected_modules: list[str], workspace: Path) -> None:
|
|
93
|
+
unknown = sorted(set(selected_modules) - _MODULE_SPECS.keys())
|
|
94
|
+
if unknown:
|
|
95
|
+
raise ValueError(f"Unknown smart module(s): {', '.join(unknown)}")
|
|
96
|
+
self.selected_modules = list(dict.fromkeys(selected_modules))
|
|
97
|
+
self.workspace = workspace
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_environment(cls, *, workspace: Path) -> SmartModuleTestRunner:
|
|
101
|
+
"""Build a runner from the JSON flags exported by the PR workflow."""
|
|
102
|
+
raw_flags = os.environ.get(_MODULE_FLAGS_ENV, "{}")
|
|
103
|
+
try:
|
|
104
|
+
flags = json.loads(raw_flags)
|
|
105
|
+
except json.JSONDecodeError as exc:
|
|
106
|
+
raise ValueError(f"{_MODULE_FLAGS_ENV} must be a JSON object of string flags") from exc
|
|
107
|
+
if not isinstance(flags, dict):
|
|
108
|
+
raise ValueError(f"{_MODULE_FLAGS_ENV} must be a JSON object of string flags")
|
|
109
|
+
unknown = sorted(set(flags) - _MODULE_SPECS.keys())
|
|
110
|
+
if unknown:
|
|
111
|
+
raise ValueError(f"{_MODULE_FLAGS_ENV} contains unknown module(s): {', '.join(unknown)}")
|
|
112
|
+
if any(not isinstance(value, str) or value not in {"true", "false"} for value in flags.values()):
|
|
113
|
+
raise ValueError(f"{_MODULE_FLAGS_ENV} values must be 'true' or 'false'")
|
|
114
|
+
return cls([name for name, enabled in flags.items() if enabled == "true"], workspace)
|
|
115
|
+
|
|
116
|
+
def pytest_args(self) -> list[str]:
|
|
117
|
+
"""Return the selected test paths followed by the original coverage scope."""
|
|
118
|
+
directories: list[str] = []
|
|
119
|
+
files: list[str] = []
|
|
120
|
+
coverage: list[str] = []
|
|
121
|
+
for name in self.selected_modules:
|
|
122
|
+
spec = _MODULE_SPECS[name]
|
|
123
|
+
for relative_path in spec.paths:
|
|
124
|
+
path = self.workspace / relative_path
|
|
125
|
+
if path.is_dir():
|
|
126
|
+
directories.append(str(path))
|
|
127
|
+
elif path.is_file():
|
|
128
|
+
files.append(str(path))
|
|
129
|
+
for pattern in spec.globs:
|
|
130
|
+
files.extend(str(path) for path in sorted(self.workspace.glob(pattern)) if path.is_file())
|
|
131
|
+
coverage.extend(f"--cov={package}" for package in spec.coverage)
|
|
132
|
+
|
|
133
|
+
# Directories must precede standalone files to avoid pytest package-node caching (#1073).
|
|
134
|
+
test_paths = list(dict.fromkeys([*directories, *files]))
|
|
135
|
+
if not test_paths:
|
|
136
|
+
return []
|
|
137
|
+
xdist_args: list[str] = []
|
|
138
|
+
for directory in directories:
|
|
139
|
+
xdist_args = get_xdist_args(
|
|
140
|
+
Path(directory),
|
|
141
|
+
coverage_workers=1,
|
|
142
|
+
distribution="worksteal",
|
|
143
|
+
)
|
|
144
|
+
if xdist_args:
|
|
145
|
+
break
|
|
146
|
+
return [
|
|
147
|
+
*test_paths,
|
|
148
|
+
*xdist_args,
|
|
149
|
+
*coverage,
|
|
150
|
+
"--cov-report=term-missing",
|
|
151
|
+
"--cov-report=xml",
|
|
152
|
+
"--cov-fail-under=100",
|
|
153
|
+
"--override-ini=addopts=",
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
def run(self) -> int:
|
|
157
|
+
"""Run pytest, preserving the workflow's no-tests skip and exit status."""
|
|
158
|
+
args = self.pytest_args()
|
|
159
|
+
if not args:
|
|
160
|
+
print("WARNING: No test paths found for the detected changes — nothing to run.")
|
|
161
|
+
return 0
|
|
162
|
+
path_end = next((index for index, arg in enumerate(args) if arg.startswith("-")), len(args))
|
|
163
|
+
test_paths = args[:path_end]
|
|
164
|
+
coverage_flags = [arg for arg in args if arg.startswith("--cov=")]
|
|
165
|
+
print("Test paths:", *test_paths, sep="\n ")
|
|
166
|
+
print("Coverage flags:", *coverage_flags, sep="\n ")
|
|
167
|
+
environment = os.environ.copy()
|
|
168
|
+
if not environment.get("COVERAGE_CORE"):
|
|
169
|
+
environment["COVERAGE_CORE"] = "sysmon"
|
|
170
|
+
result = subprocess.run(
|
|
171
|
+
[sys.executable, "-m", "pytest", *args],
|
|
172
|
+
cwd=str(self.workspace),
|
|
173
|
+
env=environment,
|
|
174
|
+
shell=False,
|
|
175
|
+
)
|
|
176
|
+
return result.returncode
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__": # pragma: no cover
|
|
180
|
+
raise SystemExit(SmartModuleTestRunner.from_environment(workspace=Path.cwd()).run())
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
7
8
|
|
|
8
9
|
_XDIST_TEST_FILE_THRESHOLD = 30
|
|
9
10
|
_MAX_XDIST_WORKERS = 4
|
|
@@ -14,8 +15,9 @@ def get_xdist_args(
|
|
|
14
15
|
*,
|
|
15
16
|
coverage_workers: int,
|
|
16
17
|
cpu_count: int | None = None,
|
|
18
|
+
distribution: Literal["loadfile", "worksteal"] = "loadfile",
|
|
17
19
|
) -> list[str]:
|
|
18
|
-
"""Return
|
|
20
|
+
"""Return xdist arguments for a large test directory within the CPU budget."""
|
|
19
21
|
if coverage_workers <= 0:
|
|
20
22
|
raise ValueError("coverage_workers must be a positive integer")
|
|
21
23
|
if not test_path.is_dir():
|
|
@@ -30,4 +32,4 @@ def get_xdist_args(
|
|
|
30
32
|
worker_count = min(_MAX_XDIST_WORKERS, max(available_cpus or 1, 1) // coverage_workers - 1)
|
|
31
33
|
if worker_count <= 1:
|
|
32
34
|
return []
|
|
33
|
-
return ["-n", str(worker_count), "--dist",
|
|
35
|
+
return ["-n", str(worker_count), "--dist", distribution]
|