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.
Files changed (96) hide show
  1. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/CHANGELOG.md +14 -0
  2. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/PKG-INFO +1 -1
  3. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/__init__.py +1 -1
  4. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/cargo_adapter.py +7 -3
  5. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/config.py +19 -2
  6. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_cargo_adapter.py +7 -0
  7. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_config_and_staging.py +39 -1
  8. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/.gitignore +0 -0
  9. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/LICENSE +0 -0
  10. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/README.md +0 -0
  11. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/SECURITY.md +0 -0
  12. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/docs/PRD.md +0 -0
  13. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/docs/harness-integration.md +0 -0
  14. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/claude-code-hooks/README.md +0 -0
  15. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/claude-code-hooks/bash_hook.py +0 -0
  16. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/claude-code-hooks/stop_hook.py +0 -0
  17. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/plan.md +0 -0
  18. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/skills/tdd-drive/README.md +0 -0
  19. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/skills/tdd-drive/SKILL.md +0 -0
  20. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/skills/tdd-handoff/README.md +0 -0
  21. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/examples/skills/tdd-handoff/SKILL.md +0 -0
  22. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/pyproject.toml +0 -0
  23. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/__init__.py +0 -0
  24. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/base.py +0 -0
  25. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/exec_adapter.py +0 -0
  26. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/gradle_adapter.py +0 -0
  27. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/pytest_adapter.py +0 -0
  28. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/vitest_adapter.py +0 -0
  29. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/adapters/xctest_adapter.py +0 -0
  30. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/advance.py +0 -0
  31. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/cli.py +0 -0
  32. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/contract.py +0 -0
  33. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/docs.py +0 -0
  34. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/envelope.py +0 -0
  35. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/fleet.py +0 -0
  36. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/gitutil.py +0 -0
  37. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/identity.py +0 -0
  38. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/leases.py +0 -0
  39. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/ledger.py +0 -0
  40. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/machine.py +0 -0
  41. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/plan_paths.py +0 -0
  42. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/render.py +0 -0
  43. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/snapshot.py +0 -0
  44. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/staging.py +0 -0
  45. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/src/tddcli/target_lint.py +0 -0
  46. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/conftest.py +0 -0
  47. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_advance_adoption.py +0 -0
  48. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_ancillary_files.py +0 -0
  49. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_artifact_regeneration.py +0 -0
  50. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_baseline_integrity.py +0 -0
  51. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_baseline_sanity.py +0 -0
  52. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_batch_collection.py +0 -0
  53. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_close_sweep_gates.py +0 -0
  54. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_concurrent_advance.py +0 -0
  55. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_config_drift.py +0 -0
  56. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_contract.py +0 -0
  57. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_docs_command.py +0 -0
  58. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_doctor_attribution.py +0 -0
  59. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_doctor_blockers.py +0 -0
  60. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_end_to_end.py +0 -0
  61. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_evidence_extraction.py +0 -0
  62. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_example_plan.py +0 -0
  63. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_exec_adapter.py +0 -0
  64. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_executor_attribution.py +0 -0
  65. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_executor_notes.py +0 -0
  66. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_failure_clipping.py +0 -0
  67. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_fleet.py +0 -0
  68. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_gradle_adapter.py +0 -0
  69. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_heartbeat.py +0 -0
  70. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_id_normalisation.py +0 -0
  71. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_init_detection.py +0 -0
  72. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_late_baseline.py +0 -0
  73. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_named_leases_and_timeout.py +0 -0
  74. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_pin_cycles.py +0 -0
  75. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_plan_paths.py +0 -0
  76. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_progress.py +0 -0
  77. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_project_commands.py +0 -0
  78. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_project_env.py +0 -0
  79. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_pytest_xdist_group.py +0 -0
  80. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_python_env_managers.py +0 -0
  81. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_refactor_cycles.py +0 -0
  82. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_release_surface.py +0 -0
  83. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_run_claim.py +0 -0
  84. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_sensitivity_evidence.py +0 -0
  85. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_single_project_repo.py +0 -0
  86. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_snapshot_and_identity.py +0 -0
  87. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_stub_hint.py +0 -0
  88. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_suite_overrides.py +0 -0
  89. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_target_lint.py +0 -0
  90. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_target_validation.py +0 -0
  91. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_timing_visibility.py +0 -0
  92. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_undeclared_close_gate.py +0 -0
  93. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_undeclared_dedup.py +0 -0
  94. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_vitest_adapter.py +0 -0
  95. {tdd_cli-0.12.1 → tdd_cli-0.12.2}/tests/test_worker_leases.py +0 -0
  96. {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.1
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
@@ -3,4 +3,4 @@
3
3
  State is derived from observed test execution, never asserted by the caller.
4
4
  """
5
5
 
6
- __version__ = "0.12.1"
6
+ __version__ = "0.12.2"
@@ -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
- base = self.root / pattern
263
- if base.is_dir():
264
- found.update(p for p in base.glob("*.rs") if p.is_file())
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
- if inner.startswith(pattern):
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 inner.startswith(pattern.rstrip("/") + "/"):
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