forgexa-cli 1.19.0__tar.gz → 1.20.0__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.
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/PKG-INFO +1 -1
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/forgexa_cli/daemon.py +198 -1
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/pyproject.toml +1 -1
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/README.md +0 -0
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/setup.cfg +0 -0
- {forgexa_cli-1.19.0 → forgexa_cli-1.20.0}/tests/test_auth_and_runtime_commands.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.20.0"
|
|
@@ -555,7 +555,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
555
555
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
556
556
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
557
557
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
558
|
-
DAEMON_VERSION = "1.
|
|
558
|
+
DAEMON_VERSION = "1.20.0"
|
|
559
559
|
|
|
560
560
|
|
|
561
561
|
def _detect_client_type() -> str:
|
|
@@ -5495,6 +5495,13 @@ class ProcessManager:
|
|
|
5495
5495
|
pass
|
|
5496
5496
|
if data.get("duration_ms") and "duration_seconds" not in metrics:
|
|
5497
5497
|
metrics["duration_seconds"] = data["duration_ms"] / 1000.0
|
|
5498
|
+
# Turn usage (Claude CLI result event) — baseline telemetry for
|
|
5499
|
+
# node-type turn budgeting (see test-script quality audit M0).
|
|
5500
|
+
if data.get("num_turns") and "num_turns" not in metrics:
|
|
5501
|
+
try:
|
|
5502
|
+
metrics["num_turns"] = int(data["num_turns"])
|
|
5503
|
+
except (TypeError, ValueError):
|
|
5504
|
+
pass
|
|
5498
5505
|
|
|
5499
5506
|
# ── 3. Prefer "result" type line — most authoritative ──
|
|
5500
5507
|
for data in reversed(parsed):
|
|
@@ -6697,6 +6704,146 @@ class ServerConnection:
|
|
|
6697
6704
|
# ── Main Daemon ──
|
|
6698
6705
|
|
|
6699
6706
|
|
|
6707
|
+
def _categorize_validation_issue(issue: str) -> str:
|
|
6708
|
+
"""Map a free-text validation issue to a stable category for telemetry.
|
|
6709
|
+
|
|
6710
|
+
Baseline data for the test-script quality initiative (M0): lets us rank
|
|
6711
|
+
validation failure modes without parsing free text. Order matters —
|
|
6712
|
+
specific patterns are checked before generic ones.
|
|
6713
|
+
"""
|
|
6714
|
+
text = issue.lower()
|
|
6715
|
+
if "script_content" in text:
|
|
6716
|
+
return "empty_script_content"
|
|
6717
|
+
if "script_path" in text and "does not exist" in text:
|
|
6718
|
+
return "missing_script_file"
|
|
6719
|
+
if "not valid json" in text:
|
|
6720
|
+
return "invalid_json"
|
|
6721
|
+
if "e2e" in text and "test case" in text:
|
|
6722
|
+
return "missing_e2e_case"
|
|
6723
|
+
if "coverage" in text:
|
|
6724
|
+
return "coverage_gap"
|
|
6725
|
+
if "scope" in text and "violation" in text:
|
|
6726
|
+
return "scope_violation"
|
|
6727
|
+
if "missing" in text or "not found" in text:
|
|
6728
|
+
return "missing_file"
|
|
6729
|
+
if "is empty" in text:
|
|
6730
|
+
return "empty_file"
|
|
6731
|
+
return "other"
|
|
6732
|
+
|
|
6733
|
+
|
|
6734
|
+
def _extract_testing_artifacts(workspace_path: Path, output_dir: str) -> dict | None:
|
|
6735
|
+
"""Summarize test-cases.json artifact counts for telemetry (best effort).
|
|
6736
|
+
|
|
6737
|
+
Returns None when the file is missing/unparseable; never raises.
|
|
6738
|
+
"""
|
|
6739
|
+
try:
|
|
6740
|
+
tc_path = workspace_path / output_dir / "test-cases.json"
|
|
6741
|
+
if not output_dir or not tc_path.exists():
|
|
6742
|
+
return None
|
|
6743
|
+
data = json.loads(tc_path.read_text(encoding="utf-8"))
|
|
6744
|
+
cases = data.get("test_cases") or []
|
|
6745
|
+
by_type: dict[str, int] = {}
|
|
6746
|
+
scripted = 0
|
|
6747
|
+
e2e_scripted = 0
|
|
6748
|
+
for case in cases:
|
|
6749
|
+
if not isinstance(case, dict):
|
|
6750
|
+
continue
|
|
6751
|
+
case_type = (case.get("test_type") or "unknown").lower()
|
|
6752
|
+
by_type[case_type] = by_type.get(case_type, 0) + 1
|
|
6753
|
+
if (case.get("script_content") or "").strip():
|
|
6754
|
+
scripted += 1
|
|
6755
|
+
if case_type == "e2e":
|
|
6756
|
+
e2e_scripted += 1
|
|
6757
|
+
return {
|
|
6758
|
+
"total_cases": len(cases),
|
|
6759
|
+
"by_type": by_type,
|
|
6760
|
+
"scripted_cases": scripted,
|
|
6761
|
+
"e2e_scripted": e2e_scripted,
|
|
6762
|
+
}
|
|
6763
|
+
except Exception:
|
|
6764
|
+
logger.debug("testing artifact telemetry extraction failed", exc_info=True)
|
|
6765
|
+
return None
|
|
6766
|
+
|
|
6767
|
+
|
|
6768
|
+
def _validate_test_evidence(
|
|
6769
|
+
base: Path, workspace_path: Path, e2e_case_ids: list[str], max_anchor_checks: int = 50
|
|
6770
|
+
) -> list[str]:
|
|
6771
|
+
"""Validate the M1 evidence bundle (test-evidence.json) for e2e test cases.
|
|
6772
|
+
|
|
6773
|
+
Deterministic, zero-environment checks: file exists and parses, locators/
|
|
6774
|
+
routes are present, every e2e case has a case_evidence entry, and each
|
|
6775
|
+
evidence anchor is a literal string that actually appears in its declared
|
|
6776
|
+
source_file (rejects invented selectors/routes). Items explicitly marked
|
|
6777
|
+
runtime_evidence_required are exempt from anchor checks by design.
|
|
6778
|
+
"""
|
|
6779
|
+
issues: list[str] = []
|
|
6780
|
+
ev_path = base / "test-evidence.json"
|
|
6781
|
+
if not ev_path.exists():
|
|
6782
|
+
return [
|
|
6783
|
+
"test-evidence.json not found. It is MANDATORY when test-intent.json "
|
|
6784
|
+
"has e2e intents: record every route/selector used by E2E scripts with "
|
|
6785
|
+
"source_file + anchor (a literal string copied from that file), plus "
|
|
6786
|
+
"case_evidence mapping each e2e test case to its route/locator refs."
|
|
6787
|
+
]
|
|
6788
|
+
try:
|
|
6789
|
+
ev = json.loads(ev_path.read_text(encoding="utf-8"))
|
|
6790
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
6791
|
+
return [f"test-evidence.json is not valid JSON: {e}"]
|
|
6792
|
+
|
|
6793
|
+
locators = ev.get("locators") or []
|
|
6794
|
+
routes = ev.get("routes") or []
|
|
6795
|
+
if not locators and not routes:
|
|
6796
|
+
issues.append(
|
|
6797
|
+
"test-evidence.json has no 'locators' or 'routes' entries — E2E scripts "
|
|
6798
|
+
"must ground their selectors/routes in source-file evidence."
|
|
6799
|
+
)
|
|
6800
|
+
|
|
6801
|
+
case_evidence = ev.get("case_evidence") or []
|
|
6802
|
+
covered_ids = {
|
|
6803
|
+
str(entry.get("test_case_id"))
|
|
6804
|
+
for entry in case_evidence
|
|
6805
|
+
if isinstance(entry, dict) and entry.get("test_case_id")
|
|
6806
|
+
}
|
|
6807
|
+
missing_cases = [cid for cid in e2e_case_ids if cid not in covered_ids]
|
|
6808
|
+
if missing_cases:
|
|
6809
|
+
issues.append(
|
|
6810
|
+
f"test-evidence.json missing case_evidence for e2e test cases: "
|
|
6811
|
+
f"{missing_cases[:10]}. Each e2e test case needs a case_evidence entry "
|
|
6812
|
+
f"with route_refs/locator_refs."
|
|
6813
|
+
)
|
|
6814
|
+
|
|
6815
|
+
checked = 0
|
|
6816
|
+
for item in list(locators) + list(routes):
|
|
6817
|
+
if checked >= max_anchor_checks:
|
|
6818
|
+
break
|
|
6819
|
+
if not isinstance(item, dict) or item.get("runtime_evidence_required"):
|
|
6820
|
+
continue
|
|
6821
|
+
checked += 1
|
|
6822
|
+
label = item.get("selector") or item.get("path") or item.get("id") or "?"
|
|
6823
|
+
src = (item.get("source_file") or "").strip()
|
|
6824
|
+
anchor = (item.get("anchor") or "").strip()
|
|
6825
|
+
if not src or not anchor:
|
|
6826
|
+
issues.append(
|
|
6827
|
+
f"evidence item '{label}' missing 'source_file' or 'anchor' — "
|
|
6828
|
+
f"anchor must be a literal string copied from source_file."
|
|
6829
|
+
)
|
|
6830
|
+
continue
|
|
6831
|
+
fpath = workspace_path / src
|
|
6832
|
+
if not fpath.exists():
|
|
6833
|
+
issues.append(f"evidence source_file not found: '{src}' (for '{label}')")
|
|
6834
|
+
continue
|
|
6835
|
+
try:
|
|
6836
|
+
content = fpath.read_text(encoding="utf-8", errors="replace")
|
|
6837
|
+
except OSError:
|
|
6838
|
+
continue
|
|
6839
|
+
if anchor not in content:
|
|
6840
|
+
issues.append(
|
|
6841
|
+
f"evidence anchor not found in '{src}': '{anchor[:80]}' (for '{label}'). "
|
|
6842
|
+
f"Copy the exact literal from the source file — do not paraphrase."
|
|
6843
|
+
)
|
|
6844
|
+
return issues
|
|
6845
|
+
|
|
6846
|
+
|
|
6700
6847
|
class RuntimeDaemon:
|
|
6701
6848
|
"""Main Daemon orchestrator — runs on the host machine.
|
|
6702
6849
|
|
|
@@ -8457,6 +8604,7 @@ class RuntimeDaemon:
|
|
|
8457
8604
|
# Not gated by _requires_structured_artifacts so that bugfix with e2e
|
|
8458
8605
|
# intents gets the same E2E chain validation as feature/improvement.
|
|
8459
8606
|
_e2e_intent_ids: set[str] = set()
|
|
8607
|
+
_e2e_case_ids: list[str] = []
|
|
8460
8608
|
_analysis_dir = (
|
|
8461
8609
|
_input.get("analysis_output_dir")
|
|
8462
8610
|
or (_input.get("context") or {}).get("analysis_output_dir")
|
|
@@ -8493,6 +8641,13 @@ class RuntimeDaemon:
|
|
|
8493
8641
|
try:
|
|
8494
8642
|
tc_data = _json.loads(tc_path.read_text(encoding="utf-8"))
|
|
8495
8643
|
cases = tc_data.get("test_cases", [])
|
|
8644
|
+
_e2e_case_ids = [
|
|
8645
|
+
str(_tc.get("id"))
|
|
8646
|
+
for _tc in cases
|
|
8647
|
+
if isinstance(_tc, dict)
|
|
8648
|
+
and (_tc.get("test_type") or "").lower() == "e2e"
|
|
8649
|
+
and _tc.get("id")
|
|
8650
|
+
]
|
|
8496
8651
|
if not cases:
|
|
8497
8652
|
issues.append("test-cases.json exists but contains no test cases")
|
|
8498
8653
|
else:
|
|
@@ -8580,6 +8735,15 @@ class RuntimeDaemon:
|
|
|
8580
8735
|
elif _requires_structured_artifacts or _requires_e2e_artifacts:
|
|
8581
8736
|
issues.append(f"coverage-matrix.json not found in {doc_dir or 'workspace root'}")
|
|
8582
8737
|
|
|
8738
|
+
# --- test-evidence.json validation (M1 evidence bundle) ---
|
|
8739
|
+
# Required whenever e2e intents exist: every route/selector
|
|
8740
|
+
# used by E2E scripts must be grounded in source-file evidence
|
|
8741
|
+
# (anchor literals are verified against the declared files).
|
|
8742
|
+
if _requires_e2e_artifacts:
|
|
8743
|
+
issues.extend(
|
|
8744
|
+
_validate_test_evidence(base, workspace_path, _e2e_case_ids)
|
|
8745
|
+
)
|
|
8746
|
+
|
|
8583
8747
|
# --- test-report.md validation ---
|
|
8584
8748
|
report_path = base / "test-report.md"
|
|
8585
8749
|
if not report_path.exists():
|
|
@@ -8897,6 +9061,9 @@ class RuntimeDaemon:
|
|
|
8897
9061
|
)
|
|
8898
9062
|
issues = self._validate_outputs(workspace_path, task, result)
|
|
8899
9063
|
if not issues:
|
|
9064
|
+
self._record_testing_telemetry(
|
|
9065
|
+
workspace_path, task, result, retries_used=attempt
|
|
9066
|
+
)
|
|
8900
9067
|
return result # All validations passed
|
|
8901
9068
|
|
|
8902
9069
|
issues_text = "\n".join(f"- {iss}" for iss in issues)
|
|
@@ -9031,8 +9198,38 @@ class RuntimeDaemon:
|
|
|
9031
9198
|
len(remaining), max_retries, task.task_id,
|
|
9032
9199
|
)
|
|
9033
9200
|
result.metrics["validation_issues"] = remaining
|
|
9201
|
+
self._record_testing_telemetry(workspace_path, task, result, retries_used=max_retries)
|
|
9034
9202
|
return result
|
|
9035
9203
|
|
|
9204
|
+
def _record_testing_telemetry(
|
|
9205
|
+
self, workspace_path: Path, task: TaskInfo, result: TaskResult, retries_used: int
|
|
9206
|
+
) -> None:
|
|
9207
|
+
"""Attach validation/testing telemetry to result.metrics (best effort).
|
|
9208
|
+
|
|
9209
|
+
M0 baseline for the test-script quality initiative: validation retry
|
|
9210
|
+
usage, stable failure categories, and testing artifact counts. These
|
|
9211
|
+
travel with the completion payload (metrics are kept in full by the
|
|
9212
|
+
backend) so baselines can be built without parsing free text.
|
|
9213
|
+
Never raises.
|
|
9214
|
+
"""
|
|
9215
|
+
try:
|
|
9216
|
+
result.metrics["validation_retries_used"] = retries_used
|
|
9217
|
+
issues = result.metrics.get("validation_issues") or []
|
|
9218
|
+
if issues:
|
|
9219
|
+
result.metrics["validation_issue_categories"] = sorted(
|
|
9220
|
+
{_categorize_validation_issue(str(issue)) for issue in issues}
|
|
9221
|
+
)
|
|
9222
|
+
if task.node_type != "testing":
|
|
9223
|
+
return
|
|
9224
|
+
input_data = task.input_data or {}
|
|
9225
|
+
context = input_data.get("context") or {}
|
|
9226
|
+
output_dir = input_data.get("output_dir") or context.get("output_dir") or ""
|
|
9227
|
+
artifacts = _extract_testing_artifacts(workspace_path, output_dir)
|
|
9228
|
+
if artifacts:
|
|
9229
|
+
result.metrics["testing_artifacts"] = artifacts
|
|
9230
|
+
except Exception:
|
|
9231
|
+
logger.debug("testing telemetry recording failed", exc_info=True)
|
|
9232
|
+
|
|
9036
9233
|
async def _collect_analysis_artifacts(
|
|
9037
9234
|
self, workspace_path: Path, task: TaskInfo, result: TaskResult
|
|
9038
9235
|
) -> None:
|
|
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
|