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.
@@ -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.552'
22
- __version_tuple__ = version_tuple = (0, 2, 552)
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 loadfile xdist arguments for a large test directory within the CPU budget."""
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", "loadfile"]
35
+ return ["-n", str(worker_count), "--dist", distribution]