easy-coding-harness 1.1.0-beta.0 → 1.1.0-beta.1

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.
@@ -125,8 +125,8 @@ WIDE_WORKFLOW_CONTRACT_PATTERN = re.compile(
125
125
  )
126
126
  DEFAULT_APPROVAL_MODE = "guard"
127
127
  DEFAULT_WORKFLOW_MODE = "adaptive"
128
- DEFAULT_TDD_ENABLED = False
129
- DEFAULT_TDD_COVERAGE_THRESHOLD = 90
128
+ DEFAULT_UNIT_TEST_MODE = "none"
129
+ DEFAULT_UT_COVERAGE_THRESHOLD = 90
130
130
  TDD_READINESS_SCHEMA = "easy-coding/tdd-readiness-v1"
131
131
  TDD_READINESS_SCOPE = "changed-production-lines"
132
132
  TDD_READINESS_PATH = Path(".easy-coding/tdd/readiness.json")
@@ -458,7 +458,7 @@ def read_memory_config(root: Path) -> dict[str, int]:
458
458
  return config
459
459
 
460
460
 
461
- def parse_tdd_threshold(value: object, source: str) -> int:
461
+ def parse_ut_threshold(value: object, source: str) -> int:
462
462
  if isinstance(value, bool):
463
463
  raise StateError(f"Invalid {source}: expected an integer from 1 to 100.")
464
464
  try:
@@ -470,18 +470,13 @@ def parse_tdd_threshold(value: object, source: str) -> int:
470
470
  return threshold
471
471
 
472
472
 
473
- def parse_yaml_bool(value: str | None, source: str) -> bool:
474
- if value is None:
475
- return DEFAULT_TDD_ENABLED
476
- normalized = value.lower()
477
- if normalized in {"true", "yes", "on"}:
478
- return True
479
- if normalized in {"false", "no", "off"}:
480
- return False
481
- raise StateError(f"Invalid {source}: expected true or false.")
473
+ def parse_unit_test_mode(value: object, source: str) -> str:
474
+ if not isinstance(value, str) or value not in {"none", "ut", "tdd"}:
475
+ raise StateError(f"Invalid {source}: expected none, ut, or tdd.")
476
+ return str(value)
482
477
 
483
478
 
484
- def read_project_behavior(root: Path) -> tuple[str, str, bool, int]:
479
+ def read_project_behavior(root: Path) -> tuple[str, str, str, int]:
485
480
  path = root / ".easy-coding" / "config.yaml"
486
481
  try:
487
482
  lines = path.read_text(encoding="utf-8").splitlines()
@@ -489,8 +484,8 @@ def read_project_behavior(root: Path) -> tuple[str, str, bool, int]:
489
484
  return (
490
485
  DEFAULT_APPROVAL_MODE,
491
486
  DEFAULT_WORKFLOW_MODE,
492
- DEFAULT_TDD_ENABLED,
493
- DEFAULT_TDD_COVERAGE_THRESHOLD,
487
+ DEFAULT_UNIT_TEST_MODE,
488
+ DEFAULT_UT_COVERAGE_THRESHOLD,
494
489
  )
495
490
 
496
491
  in_behavior = False
@@ -542,16 +537,20 @@ def read_project_behavior(root: Path) -> tuple[str, str, bool, int]:
542
537
  "Invalid behavior.workflow_mode in .easy-coding/config.yaml: "
543
538
  "expected adaptive, fast, standard, or strict."
544
539
  )
545
- if schema_version >= 4:
546
- tdd_enabled = parse_yaml_bool(behavior.get("tdd_enabled"), "behavior.tdd_enabled")
547
- tdd_threshold = parse_tdd_threshold(
548
- behavior.get("tdd_coverage_threshold", DEFAULT_TDD_COVERAGE_THRESHOLD),
549
- "behavior.tdd_coverage_threshold",
540
+ if schema_version >= 6:
541
+ unit_test_mode = parse_unit_test_mode(
542
+ behavior.get("unit_test_mode", DEFAULT_UNIT_TEST_MODE), "behavior.unit_test_mode"
543
+ )
544
+ threshold = parse_ut_threshold(
545
+ behavior.get("ut_coverage_threshold", DEFAULT_UT_COVERAGE_THRESHOLD),
546
+ "behavior.ut_coverage_threshold",
550
547
  )
551
548
  else:
552
- tdd_enabled = DEFAULT_TDD_ENABLED
553
- tdd_threshold = DEFAULT_TDD_COVERAGE_THRESHOLD
554
- return approval_mode, workflow_mode, tdd_enabled, tdd_threshold
549
+ if schema_version >= 4:
550
+ raise StateError("Run easy-coding upgrade to migrate unit-test settings to schema 6.")
551
+ unit_test_mode = DEFAULT_UNIT_TEST_MODE
552
+ threshold = DEFAULT_UT_COVERAGE_THRESHOLD
553
+ return approval_mode, workflow_mode, unit_test_mode, threshold
555
554
 
556
555
 
557
556
  def safe_tdd_report_pattern(value: object) -> bool:
@@ -723,13 +722,13 @@ def require_tdd_readiness(root: Path) -> None:
723
722
 
724
723
  def resolve_behavior(
725
724
  root: Path, session: dict
726
- ) -> tuple[str, str | None, str, str, str | None, str, bool, bool | None, bool, int, int | None, int]:
727
- project_approval, project_workflow, project_tdd, project_threshold = read_project_behavior(root)
725
+ ) -> tuple[str, str | None, str, str, str | None, str, str, str | None, str, int, int | None, int]:
726
+ project_approval, project_workflow, project_unit_test, project_threshold = read_project_behavior(root)
728
727
  legacy = session.get("confirm_mode")
729
728
  session_approval = session.get("approval_mode")
730
729
  session_workflow = session.get("workflow_mode")
731
- session_tdd = session.get("tdd_enabled")
732
- session_threshold = session.get("tdd_coverage_threshold")
730
+ session_unit_test = session.get("unit_test_mode")
731
+ session_threshold = session.get("ut_coverage_threshold")
733
732
  if session_approval is None:
734
733
  if legacy == "lite":
735
734
  session_approval = "guard"
@@ -748,11 +747,11 @@ def resolve_behavior(
748
747
  raise StateError(
749
748
  "Invalid session workflow_mode: expected adaptive, fast, standard, or strict."
750
749
  )
751
- if session_tdd is not None and not isinstance(session_tdd, bool):
752
- raise StateError("Invalid session tdd_enabled: expected true or false.")
750
+ if session_unit_test is not None:
751
+ session_unit_test = parse_unit_test_mode(session_unit_test, "session unit_test_mode")
753
752
  if session_threshold is not None:
754
- session_threshold = parse_tdd_threshold(
755
- session_threshold, "session tdd_coverage_threshold"
753
+ session_threshold = parse_ut_threshold(
754
+ session_threshold, "session ut_coverage_threshold"
756
755
  )
757
756
  return (
758
757
  project_approval,
@@ -761,9 +760,9 @@ def resolve_behavior(
761
760
  project_workflow,
762
761
  str(session_workflow) if session_workflow else None,
763
762
  str(session_workflow or project_workflow),
764
- project_tdd,
765
- session_tdd,
766
- session_tdd if session_tdd is not None else project_tdd,
763
+ project_unit_test,
764
+ session_unit_test,
765
+ session_unit_test if session_unit_test is not None else project_unit_test,
767
766
  project_threshold,
768
767
  session_threshold,
769
768
  session_threshold if session_threshold is not None else project_threshold,
@@ -775,7 +774,22 @@ def resolve_approval_mode(root: Path, session: dict) -> tuple[str, str | None, s
775
774
  return behavior[0], behavior[1], behavior[2]
776
775
 
777
776
 
777
+ def migrate_unit_test_settings(record: dict) -> bool:
778
+ changed = False
779
+ if "tdd_enabled" in record:
780
+ enabled = record.pop("tdd_enabled")
781
+ if "unit_test_mode" not in record and isinstance(enabled, bool):
782
+ record["unit_test_mode"] = "tdd" if enabled else "none"
783
+ changed = True
784
+ if "tdd_coverage_threshold" in record:
785
+ threshold = record.pop("tdd_coverage_threshold")
786
+ record.setdefault("ut_coverage_threshold", threshold)
787
+ changed = True
788
+ return changed
789
+
790
+
778
791
  def materialize_legacy_session_behavior(session: dict) -> None:
792
+ migrate_unit_test_settings(session)
779
793
  legacy = session.get("confirm_mode")
780
794
  if legacy == "lite":
781
795
  session.setdefault("approval_mode", "guard")
@@ -1264,7 +1278,7 @@ def normalize_legacy_stage(stage: object) -> object:
1264
1278
  def normalize_legacy_task(task: dict) -> bool:
1265
1279
  """Normalize legacy task state without touching artifacts outside task.json."""
1266
1280
  legacy_status = str(task.get("status") or "")
1267
- changed = False
1281
+ changed = migrate_unit_test_settings(task)
1268
1282
 
1269
1283
  for field in ("created_by", "last_agent"):
1270
1284
  normalized_agent = canonical_agent_identity(
@@ -2889,7 +2903,7 @@ def tdd_baseline_marker_reasons(
2889
2903
  def contains_tdd_threshold(content: str, threshold: int) -> bool:
2890
2904
  return re.search(
2891
2905
  rf"(?<!\d){threshold}\s*%|--threshold(?:\s+|=){threshold}(?!\d)|"
2892
- rf"tdd_coverage_threshold\s*[:=]\s*{threshold}(?!\d)",
2906
+ rf"ut_coverage_threshold\s*[:=]\s*{threshold}(?!\d)",
2893
2907
  content,
2894
2908
  re.IGNORECASE,
2895
2909
  ) is not None
@@ -3286,11 +3300,22 @@ def canonical_repository_fingerprints(root: Path, task_id: str, task: dict) -> d
3286
3300
  }
3287
3301
 
3288
3302
 
3303
+ def unit_test_contract(task: dict) -> dict:
3304
+ mode = task.get("unit_test_mode")
3305
+ # 旧证据的序列化键保持不变,配置字段改名不触发全局失效;UT 单独标识。
3306
+ contract = {
3307
+ "tdd_enabled": None if mode is None else mode != "none",
3308
+ "tdd_coverage_threshold": task.get("ut_coverage_threshold"),
3309
+ "tdd_baselines": task.get("tdd_baselines"),
3310
+ }
3311
+ if mode == "ut":
3312
+ contract["unit_test_mode"] = "ut"
3313
+ return contract
3314
+
3315
+
3289
3316
  def behavior_config_fingerprint(root: Path, task: dict | None = None) -> str:
3290
3317
  # 审批方式、记忆策略等配置不影响已经执行的测试。
3291
- return digest({key: (task or {}).get(key) for key in (
3292
- "tdd_enabled", "tdd_coverage_threshold", "tdd_baselines"
3293
- )})
3318
+ return digest(unit_test_contract(task or {}))
3294
3319
 
3295
3320
 
3296
3321
  def evidence_fingerprints(root: Path, task_id: str) -> dict[str, str]:
@@ -4205,12 +4230,10 @@ def verification_contract_fingerprint(root: Path, task_id: str, task: dict) -> s
4205
4230
  source = task.get("spec_source") if isinstance(task.get("spec_source"), dict) else {}
4206
4231
  contract = {
4207
4232
  "workflow_mode": task.get("workflow_mode"),
4208
- "tdd_enabled": task.get("tdd_enabled"),
4209
- "tdd_coverage_threshold": task.get("tdd_coverage_threshold"),
4210
- "tdd_baselines": task.get("tdd_baselines"),
4233
+ **unit_test_contract(task),
4211
4234
  **({"tdd_infrastructure": tdd_infrastructure_fingerprint(
4212
4235
  {root.resolve(), *task_repository_roots(root, task, plan)}
4213
- )} if task.get("tdd_enabled") is True else {}),
4236
+ )} if task.get("unit_test_mode") in {"ut", "tdd"} else {}),
4214
4237
  "plan": plan,
4215
4238
  "canonical": {
4216
4239
  "schema": source.get("schema"),
@@ -5134,7 +5157,7 @@ def validate_review_readiness(
5134
5157
  raise StateError(
5135
5158
  "QUALITY cannot advance while a review dimension is not passed or has error findings."
5136
5159
  )
5137
- if task.get("tdd_enabled") is True:
5160
+ if task.get("unit_test_mode") == "tdd":
5138
5161
  if is_spec_task:
5139
5162
  missing_tdd_reviews = sorted(
5140
5163
  source_task_id
@@ -5206,7 +5229,7 @@ def validate_verification_readiness(
5206
5229
  and is_non_empty_string(record.get("check"))
5207
5230
  ):
5208
5231
  if (
5209
- task.get("tdd_enabled") is True
5232
+ task.get("unit_test_mode") in {"ut", "tdd"}
5210
5233
  and record.get("check_type") == "coverage"
5211
5234
  and record.get("coverage_scope") == "gitlab"
5212
5235
  ):
@@ -5309,13 +5332,13 @@ def validate_verification_readiness(
5309
5332
  "TDD initialization cannot advance to MEMORY until readiness passes: "
5310
5333
  + "; ".join(str(reason) for reason in readiness["reasons"])
5311
5334
  )
5312
- if task.get("tdd_enabled") is not True and any(
5335
+ if task.get("unit_test_mode") not in {"ut", "tdd"} and any(
5313
5336
  record.get("check_type") == "coverage" for record in latest_by_check.values()
5314
5337
  ):
5315
5338
  raise StateError(
5316
- "Coverage verification evidence is not allowed when the frozen TDD mode is off."
5339
+ "Coverage verification evidence is not allowed when the frozen unit test mode is none."
5317
5340
  )
5318
- if task.get("tdd_enabled") is True:
5341
+ if task.get("unit_test_mode") in {"ut", "tdd"}:
5319
5342
  require_tdd_readiness(root)
5320
5343
  test_records = [
5321
5344
  record
@@ -5330,7 +5353,7 @@ def validate_verification_readiness(
5330
5353
  ]
5331
5354
  if not coverage_records:
5332
5355
  raise StateError(
5333
- "TDD verification requires changed-production-line JaCoCo coverage evidence."
5356
+ "Unit test verification requires changed-production-line JaCoCo coverage evidence."
5334
5357
  )
5335
5358
  if is_spec_task:
5336
5359
  tested_source_tasks = {
@@ -5341,7 +5364,7 @@ def validate_verification_readiness(
5341
5364
  )
5342
5365
  if missing_test_tasks:
5343
5366
  raise StateError(
5344
- "TDD Canonical verification requires local unit-test evidence for every selected source task: "
5367
+ "Unit test Canonical verification requires local unit-test evidence for every selected source task: "
5345
5368
  + ", ".join(missing_test_tasks)
5346
5369
  )
5347
5370
  covered_source_tasks = {
@@ -5352,33 +5375,33 @@ def validate_verification_readiness(
5352
5375
  )
5353
5376
  if missing_coverage_tasks:
5354
5377
  raise StateError(
5355
- "TDD Canonical verification requires separate coverage evidence for every selected source task: "
5378
+ "Unit test Canonical verification requires separate coverage evidence for every selected source task: "
5356
5379
  + ", ".join(missing_coverage_tasks)
5357
5380
  )
5358
5381
  elif not test_records:
5359
5382
  raise StateError(
5360
- "TDD verification requires passed local unit-test evidence."
5383
+ "Unit test verification requires passed local unit-test evidence."
5361
5384
  )
5362
5385
  for record in coverage_records:
5363
5386
  scope = str(record.get("coverage_scope") or "")
5364
5387
  if scope != "local":
5365
5388
  raise StateError(
5366
- "TDD coverage evidence must identify coverage_scope as local."
5389
+ "Unit test coverage evidence must identify coverage_scope as local."
5367
5390
  )
5368
- expected_threshold = task.get("tdd_coverage_threshold")
5391
+ expected_threshold = task.get("ut_coverage_threshold")
5369
5392
  expected_baselines = task.get("tdd_baselines")
5370
5393
  if (
5371
5394
  type(expected_threshold) is not int
5372
5395
  or expected_threshold < 1
5373
5396
  or expected_threshold > 100
5374
5397
  ):
5375
- raise StateError("TDD task is missing a valid frozen coverage threshold.")
5398
+ raise StateError("Unit test task is missing a valid frozen coverage threshold.")
5376
5399
  if not isinstance(expected_baselines, dict) or not expected_baselines:
5377
- raise StateError("TDD task is missing frozen Git baselines.")
5400
+ raise StateError("Unit test task is missing frozen Git baselines.")
5378
5401
  for record in coverage_records:
5379
5402
  coverage = record.get("coverage")
5380
5403
  if not isinstance(coverage, dict):
5381
- raise StateError("TDD coverage evidence must include the coverage result object.")
5404
+ raise StateError("Unit test coverage evidence must include the coverage result object.")
5382
5405
  total = coverage.get("total_lines")
5383
5406
  covered = coverage.get("covered_lines")
5384
5407
  percentage = coverage.get("percentage")
@@ -5414,7 +5437,7 @@ def validate_verification_readiness(
5414
5437
  )
5415
5438
  ):
5416
5439
  raise StateError(
5417
- "TDD coverage evidence must preserve the exact gate command, baseline, counts, percentage, frozen threshold, reports, and report fingerprint."
5440
+ "Unit test coverage evidence must preserve the exact gate command, baseline, counts, percentage, frozen threshold, reports, and report fingerprint."
5418
5441
  )
5419
5442
  if total == 0:
5420
5443
  if record.get("applicable") is not False or record.get("passed") is not True:
@@ -5423,7 +5446,7 @@ def validate_verification_readiness(
5423
5446
  )
5424
5447
  elif abs(percentage - round(covered * 100.0 / total, 2)) > 0.01:
5425
5448
  raise StateError(
5426
- "TDD coverage evidence percentage does not match covered/total counts."
5449
+ "Unit test coverage evidence percentage does not match covered/total counts."
5427
5450
  )
5428
5451
  elif (
5429
5452
  record.get("applicable") is False
@@ -5431,7 +5454,7 @@ def validate_verification_readiness(
5431
5454
  or percentage < threshold
5432
5455
  ):
5433
5456
  raise StateError(
5434
- f"TDD changed-line coverage must meet the frozen {threshold}% threshold."
5457
+ f"Unit test changed-line coverage must meet the frozen {threshold}% threshold."
5435
5458
  )
5436
5459
  if task.get("workflow_mode") == "strict":
5437
5460
  if is_spec_task:
@@ -5643,7 +5666,7 @@ def quality_repair_failures_for_window(
5643
5666
  "implementation_fingerprint"
5644
5667
  ) == implementation and record.get("config_fingerprint") == config:
5645
5668
  if (
5646
- task.get("tdd_enabled") is True
5669
+ task.get("unit_test_mode") in {"ut", "tdd"}
5647
5670
  and record.get("check_type") == "coverage"
5648
5671
  and record.get("coverage_scope") == "gitlab"
5649
5672
  ):
@@ -6408,10 +6431,18 @@ def validate_analysis_readiness(
6408
6431
  test_strategy = task_dir / "test-strategy.md"
6409
6432
  reasons: list[str] = []
6410
6433
  behavior = resolve_behavior(root, session or default_session())
6411
- tdd_enabled = behavior[8] if task_type != TDD_INIT_TASK_TYPE else False
6434
+ unit_test_mode = behavior[8] if task_type != TDD_INIT_TASK_TYPE else "none"
6412
6435
  tdd_threshold = behavior[11]
6413
6436
 
6414
- if dev_spec.is_file() and not tdd_enabled:
6437
+ if unit_test_mode == "ut":
6438
+ require_tdd_readiness(root)
6439
+ plan = latest_execution_plan(root, task_id) or {}
6440
+ if not any(str(file).endswith(".java") for unit in plan.get("units", []) for file in unit.get("files", [])):
6441
+ reasons.append("UT is enabled but the confirmed implementation scope has no Java source")
6442
+ if reasons:
6443
+ raise StateError("; ".join(reasons))
6444
+
6445
+ if dev_spec.is_file() and unit_test_mode != "tdd":
6415
6446
  compact = dev_spec.read_text(encoding="utf-8")
6416
6447
  if compact.startswith("<!-- easy-coding:compact -->"):
6417
6448
  mode, _ = calculate_workflow_floor(root, task_id)
@@ -6527,7 +6558,7 @@ def validate_analysis_readiness(
6527
6558
  plan_is_valid = has_valid_execution_plan(root, task_id)
6528
6559
  if not plan_is_valid:
6529
6560
  reasons.append("execution.jsonl has no valid plan record")
6530
- if tdd_enabled:
6561
+ if unit_test_mode == "tdd":
6531
6562
  readiness = tdd_readiness(root)
6532
6563
  if readiness["status"] != "ready":
6533
6564
  reasons.append(
@@ -6590,6 +6621,8 @@ def validate_analysis_readiness(
6590
6621
  dev_spec_content, strategy_content, baselines
6591
6622
  )
6592
6623
  )
6624
+ elif unit_test_mode == "ut":
6625
+ pass
6593
6626
  elif task_type == TDD_INIT_TASK_TYPE:
6594
6627
  try:
6595
6628
  strategy_content = test_strategy.read_text(encoding="utf-8")
@@ -6939,12 +6972,12 @@ def snapshot_state(
6939
6972
  project_workflow_mode,
6940
6973
  session_workflow_mode,
6941
6974
  configured_workflow_mode,
6942
- project_tdd_enabled,
6943
- session_tdd_enabled,
6944
- effective_tdd_enabled,
6945
- project_tdd_coverage_threshold,
6946
- session_tdd_coverage_threshold,
6947
- effective_tdd_coverage_threshold,
6975
+ project_unit_test_mode,
6976
+ session_unit_test_mode,
6977
+ effective_unit_test_mode,
6978
+ project_ut_coverage_threshold,
6979
+ session_ut_coverage_threshold,
6980
+ effective_ut_coverage_threshold,
6948
6981
  ) = resolve_behavior(root, resolved_session)
6949
6982
  concrete_workflow_mode = None
6950
6983
  if task:
@@ -6952,26 +6985,26 @@ def snapshot_state(
6952
6985
  proposal = task.get("workflow_mode_proposal")
6953
6986
  if concrete_workflow_mode is None and isinstance(proposal, dict):
6954
6987
  concrete_workflow_mode = proposal.get("selected_mode")
6955
- task_tdd_enabled = task.get("tdd_enabled") if task else None
6956
- task_tdd_coverage_threshold = task.get("tdd_coverage_threshold") if task else None
6957
- frozen_tdd = bool(
6988
+ task_unit_test_mode = task.get("unit_test_mode") if task else None
6989
+ task_ut_coverage_threshold = task.get("ut_coverage_threshold") if task else None
6990
+ frozen_unit_test = bool(
6958
6991
  task
6959
6992
  and status not in {"ANALYSIS", "INIT"}
6960
- and isinstance(task_tdd_enabled, bool)
6993
+ and task_unit_test_mode in {"none", "ut", "tdd"}
6961
6994
  )
6962
6995
  is_tdd_init = bool(
6963
6996
  task and str(task.get("type") or "").strip().lower() == TDD_INIT_TASK_TYPE
6964
6997
  )
6965
- displayed_tdd_enabled = (
6966
- False if is_tdd_init else task_tdd_enabled if frozen_tdd else effective_tdd_enabled
6998
+ displayed_unit_test_mode = (
6999
+ "none" if is_tdd_init else task_unit_test_mode if frozen_unit_test else effective_unit_test_mode
6967
7000
  )
6968
- displayed_tdd_threshold = (
6969
- task_tdd_coverage_threshold
6970
- if frozen_tdd and isinstance(task_tdd_coverage_threshold, int)
6971
- else effective_tdd_coverage_threshold
7001
+ displayed_ut_threshold = (
7002
+ task_ut_coverage_threshold
7003
+ if frozen_unit_test and isinstance(task_ut_coverage_threshold, int)
7004
+ else effective_ut_coverage_threshold
6972
7005
  )
6973
7006
  should_check_readiness = bool(
6974
- effective_tdd_enabled or task_tdd_enabled is True or is_tdd_init
7007
+ effective_unit_test_mode in {"ut", "tdd"} or task_unit_test_mode in {"ut", "tdd"} or is_tdd_init
6975
7008
  )
6976
7009
  readiness = (
6977
7010
  tdd_readiness(root)
@@ -6998,19 +7031,19 @@ def snapshot_state(
6998
7031
  "session_workflow_mode": session_workflow_mode,
6999
7032
  "configured_workflow_mode": configured_workflow_mode,
7000
7033
  "concrete_workflow_mode": concrete_workflow_mode,
7001
- "project_tdd_enabled": project_tdd_enabled,
7002
- "session_tdd_enabled": session_tdd_enabled,
7003
- "effective_tdd_enabled": effective_tdd_enabled,
7004
- "project_tdd_coverage_threshold": project_tdd_coverage_threshold,
7005
- "session_tdd_coverage_threshold": session_tdd_coverage_threshold,
7006
- "effective_tdd_coverage_threshold": effective_tdd_coverage_threshold,
7007
- "task_tdd_enabled": task_tdd_enabled,
7008
- "task_tdd_coverage_threshold": task_tdd_coverage_threshold,
7034
+ "project_unit_test_mode": project_unit_test_mode,
7035
+ "session_unit_test_mode": session_unit_test_mode,
7036
+ "effective_unit_test_mode": effective_unit_test_mode,
7037
+ "project_ut_coverage_threshold": project_ut_coverage_threshold,
7038
+ "session_ut_coverage_threshold": session_ut_coverage_threshold,
7039
+ "effective_ut_coverage_threshold": effective_ut_coverage_threshold,
7040
+ "task_unit_test_mode": task_unit_test_mode,
7041
+ "task_ut_coverage_threshold": task_ut_coverage_threshold,
7009
7042
  "task_tdd_baselines": task.get("tdd_baselines") if task else None,
7010
- "displayed_tdd_enabled": displayed_tdd_enabled,
7011
- "displayed_tdd_coverage_threshold": displayed_tdd_threshold,
7012
- "tdd_readiness_status": readiness["status"],
7013
- "tdd_readiness_reasons": readiness["reasons"],
7043
+ "displayed_unit_test_mode": displayed_unit_test_mode,
7044
+ "displayed_ut_coverage_threshold": displayed_ut_threshold,
7045
+ "unit_test_readiness_status": readiness["status"],
7046
+ "unit_test_readiness_reasons": readiness["reasons"],
7014
7047
  "spec_summary": spec_task_summary(task),
7015
7048
  # Compatibility output aliases for pre-0.9 clients.
7016
7049
  "project_confirm_mode": project_approval_mode,
@@ -7043,8 +7076,8 @@ def build_status_line(
7043
7076
  approval = str(state["effective_approval_mode"]).capitalize()
7044
7077
  workflow = str(state["concrete_workflow_mode"] or state["configured_workflow_mode"]).capitalize()
7045
7078
  status_brand = f"> **Easy Coding** · **Approval: {approval}** · **Workflow: {workflow}**"
7046
- if state["displayed_tdd_enabled"] is True:
7047
- status_brand += " · **TDD**"
7079
+ if state["displayed_unit_test_mode"] in {"ut", "tdd"}:
7080
+ status_brand += f" · **{state['displayed_unit_test_mode'].upper()}**"
7048
7081
  task_id = state["current_task"]
7049
7082
  if task_id:
7050
7083
  status = str(state["status"])
@@ -7089,10 +7122,10 @@ def build_machine_breadcrumbs(
7089
7122
  ]
7090
7123
  if state.get("concrete_workflow_mode"):
7091
7124
  lines.append(f"[easy-coding:workflow-mode:{state['concrete_workflow_mode']}]")
7092
- if state.get("displayed_tdd_enabled") is True:
7093
- lines.append("[easy-coding:tdd:enabled]")
7125
+ if state.get("displayed_unit_test_mode") in {"ut", "tdd"}:
7126
+ lines.append(f"[easy-coding:unit-test-mode:{state['displayed_unit_test_mode']}]")
7094
7127
  lines.append(
7095
- f"[easy-coding:tdd-coverage-threshold:{state['displayed_tdd_coverage_threshold']}]"
7128
+ f"[easy-coding:ut-coverage-threshold:{state['displayed_ut_coverage_threshold']}]"
7096
7129
  )
7097
7130
 
7098
7131
  if task_id:
@@ -7422,42 +7455,43 @@ def clear_session_workflow_mode(
7422
7455
  return snapshot
7423
7456
 
7424
7457
 
7425
- def set_session_tdd(
7458
+ def set_session_unit_test_mode(
7426
7459
  root: Path,
7427
- enabled: bool,
7460
+ mode: str,
7428
7461
  agent: str,
7429
7462
  threshold: int | None = None,
7430
7463
  session_file: str | Path | None = None,
7431
7464
  ) -> dict:
7432
- if enabled:
7465
+ mode = parse_unit_test_mode(mode, "session unit_test_mode")
7466
+ if mode != "none":
7433
7467
  require_tdd_readiness(root)
7434
7468
  session = ensure_session(root, session_file)
7435
7469
  materialize_legacy_session_behavior(session)
7436
- session["tdd_enabled"] = enabled
7470
+ session["unit_test_mode"] = mode
7437
7471
  if threshold is not None:
7438
- session["tdd_coverage_threshold"] = parse_tdd_threshold(
7439
- threshold, "session tdd_coverage_threshold"
7472
+ session["ut_coverage_threshold"] = parse_ut_threshold(
7473
+ threshold, "session ut_coverage_threshold"
7440
7474
  )
7441
7475
  session["last_agent"] = agent
7442
7476
  write_session(root, session, session_file)
7443
7477
  snapshot = snapshot_state(root, session_file, session)
7444
- snapshot["action"] = "set-tdd"
7478
+ snapshot["action"] = "set-unit-test-mode"
7445
7479
  return snapshot
7446
7480
 
7447
7481
 
7448
- def clear_session_tdd(
7482
+ def clear_session_unit_test_mode(
7449
7483
  root: Path,
7450
7484
  agent: str,
7451
7485
  session_file: str | Path | None = None,
7452
7486
  ) -> dict:
7453
7487
  session = ensure_session(root, session_file)
7454
7488
  materialize_legacy_session_behavior(session)
7455
- session.pop("tdd_enabled", None)
7456
- session.pop("tdd_coverage_threshold", None)
7489
+ session.pop("unit_test_mode", None)
7490
+ session.pop("ut_coverage_threshold", None)
7457
7491
  session["last_agent"] = agent
7458
7492
  write_session(root, session, session_file)
7459
7493
  snapshot = snapshot_state(root, session_file, session)
7460
- snapshot["action"] = "clear-tdd"
7494
+ snapshot["action"] = "clear-unit-test-mode"
7461
7495
  return snapshot
7462
7496
 
7463
7497
 
@@ -9711,35 +9745,36 @@ def freeze_workflow_mode(
9711
9745
  task["workflow_mode_confirmed_by"] = agent
9712
9746
 
9713
9747
 
9714
- def freeze_tdd_mode(
9748
+ def freeze_unit_test_mode(
9715
9749
  root: Path, session: dict, task_id: str, task: dict, agent: str
9716
9750
  ) -> None:
9717
9751
  behavior = resolve_behavior(root, session)
9718
9752
  task_type = str(task.get("type") or "").strip().lower()
9719
- task["tdd_enabled"] = (
9720
- behavior[8] if task_type != TDD_INIT_TASK_TYPE else False
9753
+ task["unit_test_mode"] = (
9754
+ behavior[8] if task_type != TDD_INIT_TASK_TYPE else "none"
9721
9755
  )
9722
- task["tdd_coverage_threshold"] = behavior[11]
9723
- if task["tdd_enabled"] is True:
9756
+ task["ut_coverage_threshold"] = behavior[11]
9757
+ if task["unit_test_mode"] in {"ut", "tdd"}:
9724
9758
  require_tdd_readiness(root)
9725
9759
  plan = latest_execution_plan(root, task_id)
9726
9760
  if plan is None:
9727
- raise StateError("Cannot freeze TDD baseline without a valid execution plan.")
9761
+ raise StateError("Cannot freeze unit test baseline without a valid execution plan.")
9728
9762
  baselines = {
9729
9763
  key: git_head_sha(repository)
9730
9764
  for key, repository in tdd_repositories(root, task, plan).items()
9731
9765
  }
9732
- task_dir = task_json_path(root, task_id).parent
9733
- try:
9734
- dev_spec_content = (task_dir / "dev-spec.md").read_text(encoding="utf-8")
9735
- strategy_content = (task_dir / "test-strategy.md").read_text(encoding="utf-8")
9736
- except OSError as error:
9737
- raise StateError("Cannot freeze TDD without readable analysis artifacts.") from error
9738
- marker_reasons = tdd_baseline_marker_reasons(
9739
- dev_spec_content, strategy_content, baselines
9740
- )
9741
- if marker_reasons:
9742
- raise StateError("; ".join(marker_reasons))
9766
+ if task["unit_test_mode"] == "tdd":
9767
+ task_dir = task_json_path(root, task_id).parent
9768
+ try:
9769
+ dev_spec_content = (task_dir / "dev-spec.md").read_text(encoding="utf-8")
9770
+ strategy_content = (task_dir / "test-strategy.md").read_text(encoding="utf-8")
9771
+ except OSError as error:
9772
+ raise StateError("Cannot freeze TDD without readable analysis artifacts.") from error
9773
+ marker_reasons = tdd_baseline_marker_reasons(
9774
+ dev_spec_content, strategy_content, baselines
9775
+ )
9776
+ if marker_reasons:
9777
+ raise StateError("; ".join(marker_reasons))
9743
9778
  task["tdd_baselines"] = baselines
9744
9779
  else:
9745
9780
  task.pop("tdd_baselines", None)
@@ -9899,7 +9934,7 @@ def apply_transition(
9899
9934
  validate_analysis_readiness(root, resolved_task_id, session)
9900
9935
  if task.get("workflow_mode_legacy") is not True:
9901
9936
  freeze_workflow_mode(root, session, resolved_task_id, task, agent)
9902
- freeze_tdd_mode(root, session, resolved_task_id, task, agent)
9937
+ freeze_unit_test_mode(root, session, resolved_task_id, task, agent)
9903
9938
  repair_source_task_ids: set[str] | None = None
9904
9939
  quality_exit_outcome: str | None = None
9905
9940
  if previous == "QUALITY" and stage in {"IMPLEMENT", "ANALYSIS"}:
@@ -10611,13 +10646,13 @@ def main() -> int:
10611
10646
  clear_workflow_mode_parser = subcommands.add_parser("clear-workflow-mode", parents=[common])
10612
10647
  clear_workflow_mode_parser.add_argument("--agent", required=True)
10613
10648
 
10614
- set_tdd_parser = subcommands.add_parser("set-tdd", parents=[common])
10615
- set_tdd_parser.add_argument("--enabled", required=True, choices=["true", "false"])
10616
- set_tdd_parser.add_argument("--threshold", type=int)
10617
- set_tdd_parser.add_argument("--agent", required=True)
10649
+ set_unit_test_parser = subcommands.add_parser("set-unit-test-mode", parents=[common])
10650
+ set_unit_test_parser.add_argument("--mode", required=True, choices=["none", "ut", "tdd"])
10651
+ set_unit_test_parser.add_argument("--threshold", type=int)
10652
+ set_unit_test_parser.add_argument("--agent", required=True)
10618
10653
 
10619
- clear_tdd_parser = subcommands.add_parser("clear-tdd", parents=[common])
10620
- clear_tdd_parser.add_argument("--agent", required=True)
10654
+ clear_unit_test_parser = subcommands.add_parser("clear-unit-test-mode", parents=[common])
10655
+ clear_unit_test_parser.add_argument("--agent", required=True)
10621
10656
 
10622
10657
  # Compatibility aliases for pre-0.9 callers.
10623
10658
  set_confirm_mode_parser = subcommands.add_parser("set-confirm-mode", parents=[common])
@@ -11128,13 +11163,13 @@ def main() -> int:
11128
11163
  session_file,
11129
11164
  )
11130
11165
  )
11131
- elif command == "set-tdd":
11166
+ elif command == "set-unit-test-mode":
11132
11167
  emit(
11133
11168
  attach_status_context(
11134
11169
  root,
11135
- set_session_tdd(
11170
+ set_session_unit_test_mode(
11136
11171
  root,
11137
- args.enabled == "true",
11172
+ args.mode,
11138
11173
  agent,
11139
11174
  args.threshold,
11140
11175
  session_file,
@@ -11143,11 +11178,11 @@ def main() -> int:
11143
11178
  session_file,
11144
11179
  )
11145
11180
  )
11146
- elif command == "clear-tdd":
11181
+ elif command == "clear-unit-test-mode":
11147
11182
  emit(
11148
11183
  attach_status_context(
11149
11184
  root,
11150
- clear_session_tdd(root, agent, session_file),
11185
+ clear_session_unit_test_mode(root, agent, session_file),
11151
11186
  agent,
11152
11187
  session_file,
11153
11188
  )