tdd-cli 0.12.1__tar.gz → 0.12.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.
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/CHANGELOG.md +14 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/PKG-INFO +1 -1
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/__init__.py +1 -1
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/cargo_adapter.py +7 -3
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/config.py +19 -2
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_cargo_adapter.py +7 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_config_and_staging.py +39 -1
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/.gitignore +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/LICENSE +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/README.md +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/SECURITY.md +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/docs/PRD.md +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/docs/harness-integration.md +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/claude-code-hooks/README.md +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/claude-code-hooks/bash_hook.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/claude-code-hooks/stop_hook.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/plan.md +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/skills/tdd-drive/README.md +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/skills/tdd-drive/SKILL.md +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/skills/tdd-handoff/README.md +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/skills/tdd-handoff/SKILL.md +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/pyproject.toml +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/__init__.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/base.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/exec_adapter.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/gradle_adapter.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/pytest_adapter.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/vitest_adapter.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/xctest_adapter.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/advance.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/cli.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/contract.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/docs.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/envelope.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/fleet.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/gitutil.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/identity.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/leases.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/ledger.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/machine.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/plan_paths.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/render.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/snapshot.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/staging.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/target_lint.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/conftest.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_advance_adoption.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_ancillary_files.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_artifact_regeneration.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_baseline_integrity.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_baseline_sanity.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_batch_collection.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_close_sweep_gates.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_concurrent_advance.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_config_drift.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_contract.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_docs_command.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_doctor_attribution.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_doctor_blockers.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_end_to_end.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_evidence_extraction.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_example_plan.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_exec_adapter.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_executor_attribution.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_executor_notes.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_failure_clipping.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_fleet.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_gradle_adapter.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_heartbeat.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_id_normalisation.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_init_detection.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_late_baseline.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_named_leases_and_timeout.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_pin_cycles.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_plan_paths.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_progress.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_project_commands.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_project_env.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_pytest_xdist_group.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_python_env_managers.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_refactor_cycles.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_release_surface.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_run_claim.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_sensitivity_evidence.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_single_project_repo.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_snapshot_and_identity.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_stub_hint.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_suite_overrides.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_target_lint.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_target_validation.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_timing_visibility.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_undeclared_close_gate.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_undeclared_dedup.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_vitest_adapter.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_worker_leases.py +0 -0
- {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_xctest_adapter.py +0 -0
|
@@ -6,6 +6,20 @@ and the project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.12.2] - 2026-09-17
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **A wildcard directory in `test_paths` matched nothing** (#136): a pattern ending in `/` was
|
|
14
|
+
compared as a literal prefix by `Project.is_test_file` and joined as a literal path by the cargo
|
|
15
|
+
adapter's `_test_files`, so `crates/*/tests/` — how a workspace declares one pattern instead of
|
|
16
|
+
one per crate — matched no file at all. Every test file in such a project classified as
|
|
17
|
+
implementation, so a RED commit touching only tests was recorded as `implementation_during_red`,
|
|
18
|
+
reporting the agent for writing production code it had not written; `_test_files` returned an
|
|
19
|
+
empty list, leaving the unaccounted-files check with nothing to compare. A directory pattern is
|
|
20
|
+
now matched per path segment (`crates/*/tests/` covers `crates/x/tests/main.rs`, not
|
|
21
|
+
`crates/x/src/tests/main.rs`), and the adapter globs it.
|
|
22
|
+
|
|
9
23
|
## [0.12.1] - 2026-09-17
|
|
10
24
|
|
|
11
25
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tdd-cli
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.2
|
|
4
4
|
Summary: Ledger-backed TDD process controller for autonomous coding agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/geuben/tdd-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/geuben/tdd-cli
|
|
@@ -259,9 +259,13 @@ class CargoAdapter(Adapter):
|
|
|
259
259
|
found: set[Path] = set()
|
|
260
260
|
for pattern in self.project.test_patterns or []:
|
|
261
261
|
if pattern.endswith("/"):
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
262
|
+
# `self.root / pattern` is a literal path, so a wildcard
|
|
263
|
+
# directory pattern (`crates/*/tests/`) never resolves; glob it.
|
|
264
|
+
# A trailing slash is accepted by glob on every supported
|
|
265
|
+
# Python (3.11–3.14), so the pattern goes through as declared.
|
|
266
|
+
for base in self.root.glob(pattern):
|
|
267
|
+
if base.is_dir():
|
|
268
|
+
found.update(p for p in base.glob("*.rs") if p.is_file())
|
|
265
269
|
else:
|
|
266
270
|
found.update(p for p in self.root.glob(pattern) if p.is_file())
|
|
267
271
|
return sorted(found)
|
|
@@ -73,6 +73,20 @@ class Override:
|
|
|
73
73
|
timeout: int | None = None
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def _dir_pattern_matches(pattern: str, inner: str) -> bool:
|
|
77
|
+
"""True when `inner` sits under the directory `pattern` describes.
|
|
78
|
+
|
|
79
|
+
`pattern` ends in `/` and may contain wildcards; matching is per path
|
|
80
|
+
segment, so `crates/*/tests/` covers `crates/dd-bridge/tests/main.rs` but
|
|
81
|
+
not `crates/dd-bridge/src/main.rs`.
|
|
82
|
+
"""
|
|
83
|
+
segments = pattern.rstrip("/").split("/")
|
|
84
|
+
parts = inner.split("/")
|
|
85
|
+
if len(parts) <= len(segments):
|
|
86
|
+
return False
|
|
87
|
+
return all(fnmatch(part, seg) for part, seg in zip(parts, segments))
|
|
88
|
+
|
|
89
|
+
|
|
76
90
|
@dataclass
|
|
77
91
|
class Project:
|
|
78
92
|
name: str
|
|
@@ -141,11 +155,14 @@ class Project:
|
|
|
141
155
|
inner = self.relative_to_root(rel_path)
|
|
142
156
|
for pattern in self.test_patterns:
|
|
143
157
|
if pattern.endswith("/"):
|
|
144
|
-
|
|
158
|
+
# A directory pattern may carry wildcards (`crates/*/tests/`),
|
|
159
|
+
# so it is matched as a glob against the leading path segments,
|
|
160
|
+
# not compared as a literal prefix.
|
|
161
|
+
if _dir_pattern_matches(pattern, inner):
|
|
145
162
|
return True
|
|
146
163
|
elif fnmatch(inner, pattern) or fnmatch(rel_path, pattern):
|
|
147
164
|
return True
|
|
148
|
-
elif
|
|
165
|
+
elif _dir_pattern_matches(pattern.rstrip("/") + "/", inner):
|
|
149
166
|
return True
|
|
150
167
|
return False
|
|
151
168
|
|
|
@@ -582,3 +582,10 @@ def test_a_crate_with_no_manifest_keeps_cargos_default_mapping(tmp_path):
|
|
|
582
582
|
assert a._manifest_targets() == {}
|
|
583
583
|
assert a.target_path("roundtrip::renders_cover_only") == "tests/roundtrip.rs"
|
|
584
584
|
assert a._target_of_file("tests/roundtrip.rs") == "roundtrip"
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def test_test_files_expands_a_wildcard_directory_pattern(tmp_path):
|
|
588
|
+
"""`self.root / "crates/*/tests/"` is a literal path and never resolves."""
|
|
589
|
+
a = make_consolidated_adapter(tmp_path)
|
|
590
|
+
rels = sorted(str(p.relative_to(tmp_path)) for p in a._test_files())
|
|
591
|
+
assert rels == ["crates/dd-bridge/tests/main.rs", "crates/dd-core/tests/main.rs"]
|
|
@@ -21,6 +21,11 @@ adapter = "vitest"
|
|
|
21
21
|
test_paths = ["e2e/"]
|
|
22
22
|
in_close_sweep = false
|
|
23
23
|
|
|
24
|
+
[project.workspace]
|
|
25
|
+
root = "ws"
|
|
26
|
+
adapter = "cargo"
|
|
27
|
+
test_paths = ["crates/*/tests/"]
|
|
28
|
+
|
|
24
29
|
[artifact.openapi]
|
|
25
30
|
path = "schema/openapi.json"
|
|
26
31
|
produced_by = "backend"
|
|
@@ -43,7 +48,7 @@ def cfg(tmp_path):
|
|
|
43
48
|
|
|
44
49
|
|
|
45
50
|
def test_projects_and_roots_are_declared_not_scanned(cfg):
|
|
46
|
-
assert sorted(cfg.projects) == ["backend", "e2e", "frontend"]
|
|
51
|
+
assert sorted(cfg.projects) == ["backend", "e2e", "frontend", "workspace"]
|
|
47
52
|
assert cfg.project("backend").root == "backend"
|
|
48
53
|
|
|
49
54
|
|
|
@@ -320,3 +325,36 @@ def test_health_command_parses_onto_project(tmp_path):
|
|
|
320
325
|
)
|
|
321
326
|
with pytest.raises(config_mod.ConfigError):
|
|
322
327
|
config_mod.load(tmp_path)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def test_a_wildcard_directory_test_path_matches_per_segment(cfg):
|
|
331
|
+
"""`crates/*/tests/` is a glob, not a literal prefix.
|
|
332
|
+
|
|
333
|
+
A workspace declares its test directories with one pattern rather than one
|
|
334
|
+
per crate. Comparing it as a literal string classifies every test file as
|
|
335
|
+
implementation, which reports the agent for writing production code during
|
|
336
|
+
RED when it only edited tests.
|
|
337
|
+
"""
|
|
338
|
+
ws = cfg.project("workspace")
|
|
339
|
+
assert ws.is_test_file("ws/crates/dd-bridge/tests/main.rs")
|
|
340
|
+
assert ws.is_test_file("ws/crates/dd-core/tests/attention_clear.rs")
|
|
341
|
+
# The wildcard spans exactly one segment, and the directory itself is not a file.
|
|
342
|
+
assert not ws.is_test_file("ws/crates/dd-bridge/src/adapter/config.rs")
|
|
343
|
+
assert not ws.is_test_file("ws/crates/dd-bridge/src/tests/inner.rs")
|
|
344
|
+
assert not ws.is_test_file("ws/crates/a/b/tests/deep.rs")
|
|
345
|
+
assert not ws.is_test_file("ws/crates/dd-bridge/tests")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def test_a_wildcard_test_path_classifies_edits_as_tests_not_implementation(cfg):
|
|
349
|
+
"""The staging consequence: RED may touch these without being reported."""
|
|
350
|
+
changed = {
|
|
351
|
+
"ws/crates/dd-bridge/tests/main.rs",
|
|
352
|
+
"ws/crates/dd-bridge/tests/adapter_config.rs",
|
|
353
|
+
"ws/crates/dd-bridge/src/adapter/config.rs",
|
|
354
|
+
}
|
|
355
|
+
out = staging.classify(cfg, changed, ["workspace"], None, set())
|
|
356
|
+
assert out.tests == [
|
|
357
|
+
"ws/crates/dd-bridge/tests/adapter_config.rs",
|
|
358
|
+
"ws/crates/dd-bridge/tests/main.rs",
|
|
359
|
+
]
|
|
360
|
+
assert out.implementation == ["ws/crates/dd-bridge/src/adapter/config.rs"]
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|