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.
- package/CHANGELOG.md +11 -0
- package/README.md +11 -10
- package/dist/cli.js +208 -165
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/common/bundled-skills/ec-init/SKILL.md +2 -2
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +6 -6
- package/templates/common/skills/ec-analysis/SKILL.md +30 -41
- package/templates/common/skills/ec-config/SKILL.md +37 -46
- package/templates/common/skills/ec-implementing/SKILL.md +4 -3
- package/templates/common/skills/ec-lite/SKILL.md +1 -1
- package/templates/common/skills/ec-memory/SKILL.md +3 -4
- package/templates/common/skills/ec-quality/SKILL.md +9 -4
- package/templates/common/skills/ec-task-management/SKILL.md +2 -2
- package/templates/common/skills/ec-tdd-init/SKILL.md +7 -7
- package/templates/common/skills/ec-workflow/SKILL.md +9 -9
- package/templates/main-constraint/AGENTS.md.tpl +15 -14
- package/templates/main-constraint/CLAUDE.md.tpl +15 -14
- package/templates/runtime/tools/easy_coding_java_coverage.py +5 -5
- package/templates/shared-hooks/easy_coding_inputs.py +1 -1
- package/templates/shared-hooks/easy_coding_state.py +173 -138
- package/templates/shared-hooks/inject-subagent-context.py +3 -3
|
@@ -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
|
-
|
|
129
|
-
|
|
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
|
|
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
|
|
474
|
-
if value
|
|
475
|
-
|
|
476
|
-
|
|
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,
|
|
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
|
-
|
|
493
|
-
|
|
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 >=
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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,
|
|
727
|
-
project_approval, project_workflow,
|
|
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
|
-
|
|
732
|
-
session_threshold = session.get("
|
|
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
|
|
752
|
-
|
|
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 =
|
|
755
|
-
session_threshold, "session
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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 =
|
|
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"
|
|
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(
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
|
5339
|
+
"Coverage verification evidence is not allowed when the frozen unit test mode is none."
|
|
5317
5340
|
)
|
|
5318
|
-
if task.get("
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
5389
|
+
"Unit test coverage evidence must identify coverage_scope as local."
|
|
5367
5390
|
)
|
|
5368
|
-
expected_threshold = task.get("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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"
|
|
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("
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
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
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
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
|
|
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
|
-
|
|
6966
|
-
|
|
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
|
-
|
|
6969
|
-
|
|
6970
|
-
if
|
|
6971
|
-
else
|
|
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
|
-
|
|
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
|
-
"
|
|
7002
|
-
"
|
|
7003
|
-
"
|
|
7004
|
-
"
|
|
7005
|
-
"
|
|
7006
|
-
"
|
|
7007
|
-
"
|
|
7008
|
-
"
|
|
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
|
-
"
|
|
7011
|
-
"
|
|
7012
|
-
"
|
|
7013
|
-
"
|
|
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["
|
|
7047
|
-
status_brand += " · **
|
|
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("
|
|
7093
|
-
lines.append("[easy-coding:
|
|
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:
|
|
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
|
|
7458
|
+
def set_session_unit_test_mode(
|
|
7426
7459
|
root: Path,
|
|
7427
|
-
|
|
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
|
-
|
|
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["
|
|
7470
|
+
session["unit_test_mode"] = mode
|
|
7437
7471
|
if threshold is not None:
|
|
7438
|
-
session["
|
|
7439
|
-
threshold, "session
|
|
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-
|
|
7478
|
+
snapshot["action"] = "set-unit-test-mode"
|
|
7445
7479
|
return snapshot
|
|
7446
7480
|
|
|
7447
7481
|
|
|
7448
|
-
def
|
|
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("
|
|
7456
|
-
session.pop("
|
|
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-
|
|
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
|
|
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["
|
|
9720
|
-
behavior[8] if task_type != TDD_INIT_TASK_TYPE else
|
|
9753
|
+
task["unit_test_mode"] = (
|
|
9754
|
+
behavior[8] if task_type != TDD_INIT_TASK_TYPE else "none"
|
|
9721
9755
|
)
|
|
9722
|
-
task["
|
|
9723
|
-
if task["
|
|
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
|
|
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
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
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
|
-
|
|
10620
|
-
|
|
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-
|
|
11166
|
+
elif command == "set-unit-test-mode":
|
|
11132
11167
|
emit(
|
|
11133
11168
|
attach_status_context(
|
|
11134
11169
|
root,
|
|
11135
|
-
|
|
11170
|
+
set_session_unit_test_mode(
|
|
11136
11171
|
root,
|
|
11137
|
-
args.
|
|
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-
|
|
11181
|
+
elif command == "clear-unit-test-mode":
|
|
11147
11182
|
emit(
|
|
11148
11183
|
attach_status_context(
|
|
11149
11184
|
root,
|
|
11150
|
-
|
|
11185
|
+
clear_session_unit_test_mode(root, agent, session_file),
|
|
11151
11186
|
agent,
|
|
11152
11187
|
session_file,
|
|
11153
11188
|
)
|