forgexa-cli 1.48.1__tar.gz → 1.48.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.
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/PKG-INFO +1 -1
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli/daemon.py +290 -15
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/pyproject.toml +1 -1
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/README.md +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli/agent_core.py +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/setup.cfg +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.48.1 → forgexa_cli-1.48.2}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.48.
|
|
2
|
+
__version__ = "1.48.2"
|
|
@@ -1120,7 +1120,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
1120
1120
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
1121
1121
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
1122
1122
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
1123
|
-
DAEMON_VERSION = "1.48.
|
|
1123
|
+
DAEMON_VERSION = "1.48.2"
|
|
1124
1124
|
|
|
1125
1125
|
|
|
1126
1126
|
def _detect_client_type() -> str:
|
|
@@ -5444,18 +5444,22 @@ class ProcessManager:
|
|
|
5444
5444
|
return "unable to run"
|
|
5445
5445
|
|
|
5446
5446
|
@staticmethod
|
|
5447
|
-
def
|
|
5448
|
-
"""Return whether an agent
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5447
|
+
def is_idle_timeout_without_work(result: "TaskResult") -> bool:
|
|
5448
|
+
"""Return whether an idle-killed agent left no work worth preserving.
|
|
5449
|
+
|
|
5450
|
+
The deciding evidence is workspace output (``files_changed``), NOT
|
|
5451
|
+
stdout/stderr chatter: an agent can emit an intro sentence and a
|
|
5452
|
+
todowrite event and then stall forever on a hung model request
|
|
5453
|
+
(SI-1453: OpenCode produced 2 output lines in 39 minutes, zero file
|
|
5454
|
+
changes). Streamed text is conversation, not work, so it must not
|
|
5455
|
+
block recovery. When no files were changed, trying an alternate
|
|
5456
|
+
agent is safe: the pre-fallback guard in the task runner re-checks
|
|
5457
|
+
the workspace (uncommitted AND committed changes since
|
|
5458
|
+
node_before_sha) before switching agents, and timeouts that occurred
|
|
5459
|
+
after real work still go through the normal recovery path.
|
|
5454
5460
|
"""
|
|
5455
5461
|
return (
|
|
5456
5462
|
result.failure_code == "agent_idle_timeout"
|
|
5457
|
-
and not result.stdout.strip()
|
|
5458
|
-
and not result.stderr.strip()
|
|
5459
5463
|
and not result.files_changed
|
|
5460
5464
|
)
|
|
5461
5465
|
|
|
@@ -8955,6 +8959,261 @@ def _validate_test_evidence(
|
|
|
8955
8959
|
return issues
|
|
8956
8960
|
|
|
8957
8961
|
|
|
8962
|
+
_QA_TEST_LEVELS = frozenset({"unit", "integration", "e2e"})
|
|
8963
|
+
|
|
8964
|
+
|
|
8965
|
+
def _parse_frozen_qa_test_level_policy(
|
|
8966
|
+
snapshot: object,
|
|
8967
|
+
) -> tuple[dict[str, Any] | None, list[str]]:
|
|
8968
|
+
"""Parse a server-owned policy snapshot without backend-package imports.
|
|
8969
|
+
|
|
8970
|
+
The daemon also runs standalone in CLI/Desktop bundles, so this mirrors the
|
|
8971
|
+
server contract with stdlib-only validation rather than importing a backend
|
|
8972
|
+
service. A missing snapshot is a historical task and intentionally keeps
|
|
8973
|
+
legacy validation behavior.
|
|
8974
|
+
"""
|
|
8975
|
+
if snapshot is None:
|
|
8976
|
+
return None, []
|
|
8977
|
+
if not isinstance(snapshot, dict):
|
|
8978
|
+
return None, [
|
|
8979
|
+
"qa_policy.configuration_invalid: frozen QA test-level policy snapshot must be an object."
|
|
8980
|
+
]
|
|
8981
|
+
policy = snapshot.get("policy")
|
|
8982
|
+
if not isinstance(policy, dict):
|
|
8983
|
+
return None, [
|
|
8984
|
+
"qa_policy.configuration_invalid: frozen QA test-level policy snapshot is missing its policy."
|
|
8985
|
+
]
|
|
8986
|
+
expected_fingerprint = hashlib.sha256(
|
|
8987
|
+
json.dumps(policy, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
8988
|
+
).hexdigest()
|
|
8989
|
+
if snapshot.get("fingerprint") != expected_fingerprint:
|
|
8990
|
+
return None, [
|
|
8991
|
+
"qa_policy.configuration_invalid: frozen QA test-level policy fingerprint does not match its payload."
|
|
8992
|
+
]
|
|
8993
|
+
if set(policy) != {"schema_version", "enforcement", "levels"}:
|
|
8994
|
+
return None, [
|
|
8995
|
+
"qa_policy.configuration_invalid: frozen QA test-level policy has unexpected fields."
|
|
8996
|
+
]
|
|
8997
|
+
if type(policy.get("schema_version")) is not int or policy["schema_version"] != 1:
|
|
8998
|
+
return None, [
|
|
8999
|
+
"qa_policy.configuration_invalid: frozen QA test-level policy schema_version is unsupported."
|
|
9000
|
+
]
|
|
9001
|
+
enforcement = policy.get("enforcement")
|
|
9002
|
+
if enforcement not in {"legacy", "enforce"}:
|
|
9003
|
+
return None, [
|
|
9004
|
+
"qa_policy.configuration_invalid: frozen QA test-level policy enforcement is invalid."
|
|
9005
|
+
]
|
|
9006
|
+
levels = policy.get("levels")
|
|
9007
|
+
if not isinstance(levels, dict) or set(levels) != _QA_TEST_LEVELS:
|
|
9008
|
+
return None, [
|
|
9009
|
+
"qa_policy.configuration_invalid: frozen QA test-level policy levels are invalid."
|
|
9010
|
+
]
|
|
9011
|
+
|
|
9012
|
+
normalized_levels: dict[str, dict[str, bool]] = {}
|
|
9013
|
+
for level in _QA_TEST_LEVELS:
|
|
9014
|
+
config = levels.get(level)
|
|
9015
|
+
if not isinstance(config, dict) or set(config) != {"enabled", "workflow_auto_run"}:
|
|
9016
|
+
return None, [
|
|
9017
|
+
"qa_policy.configuration_invalid: frozen QA test-level policy level configuration is invalid."
|
|
9018
|
+
]
|
|
9019
|
+
enabled = config.get("enabled")
|
|
9020
|
+
auto_run = config.get("workflow_auto_run")
|
|
9021
|
+
if type(enabled) is not bool or type(auto_run) is not bool:
|
|
9022
|
+
return None, [
|
|
9023
|
+
"qa_policy.configuration_invalid: frozen QA test-level policy switches must be boolean."
|
|
9024
|
+
]
|
|
9025
|
+
if auto_run and not enabled:
|
|
9026
|
+
return None, [
|
|
9027
|
+
"qa_policy.configuration_invalid: workflow_auto_run requires an enabled test level."
|
|
9028
|
+
]
|
|
9029
|
+
normalized_levels[level] = {
|
|
9030
|
+
"enabled": enabled,
|
|
9031
|
+
"workflow_auto_run": auto_run,
|
|
9032
|
+
}
|
|
9033
|
+
return {
|
|
9034
|
+
"enforcement": enforcement,
|
|
9035
|
+
"fingerprint": expected_fingerprint,
|
|
9036
|
+
"levels": normalized_levels,
|
|
9037
|
+
}, []
|
|
9038
|
+
|
|
9039
|
+
|
|
9040
|
+
def _validate_enforced_qa_test_level_artifacts(
|
|
9041
|
+
policy: dict[str, Any],
|
|
9042
|
+
test_intent_data: object,
|
|
9043
|
+
test_cases_data: object,
|
|
9044
|
+
workspace_path: Path,
|
|
9045
|
+
) -> list[str]:
|
|
9046
|
+
"""Validate a testing node's artifacts against its frozen policy snapshot."""
|
|
9047
|
+
if policy.get("enforcement") != "enforce":
|
|
9048
|
+
return []
|
|
9049
|
+
|
|
9050
|
+
issues: list[str] = []
|
|
9051
|
+
if not isinstance(test_intent_data, dict):
|
|
9052
|
+
return [
|
|
9053
|
+
"qa_policy.test_intent_missing: enforced QA policy requires test-intent.json."
|
|
9054
|
+
]
|
|
9055
|
+
declared_fingerprint = test_intent_data.get("qa_test_level_policy_fingerprint")
|
|
9056
|
+
if not isinstance(declared_fingerprint, str) or not declared_fingerprint.strip():
|
|
9057
|
+
issues.append(
|
|
9058
|
+
"qa_policy.fingerprint_missing: test-intent.json must echo the frozen QA policy fingerprint."
|
|
9059
|
+
)
|
|
9060
|
+
elif declared_fingerprint.strip() != policy["fingerprint"]:
|
|
9061
|
+
issues.append(
|
|
9062
|
+
"qa_policy.fingerprint_mismatch: test-intent.json does not match the frozen QA policy."
|
|
9063
|
+
)
|
|
9064
|
+
|
|
9065
|
+
raw_intents = test_intent_data.get("intents")
|
|
9066
|
+
if not isinstance(raw_intents, list):
|
|
9067
|
+
return issues + [
|
|
9068
|
+
"qa_policy.test_intent_invalid: test-intent.json intents must be an array."
|
|
9069
|
+
]
|
|
9070
|
+
|
|
9071
|
+
automated_intent_ids: dict[str, set[str]] = {
|
|
9072
|
+
level: set() for level in _QA_TEST_LEVELS
|
|
9073
|
+
}
|
|
9074
|
+
manual_intent_reasons: dict[str, str] = {}
|
|
9075
|
+
for index, raw_intent in enumerate(raw_intents[:200]):
|
|
9076
|
+
if not isinstance(raw_intent, dict):
|
|
9077
|
+
issues.append(f"qa_policy.test_intent_invalid: intent {index} must be an object.")
|
|
9078
|
+
continue
|
|
9079
|
+
intent_id = raw_intent.get("id") or raw_intent.get("test_id") or raw_intent.get("intent_id")
|
|
9080
|
+
if not isinstance(intent_id, str) or not intent_id.strip():
|
|
9081
|
+
issues.append(f"qa_policy.test_intent_invalid: intent {index} is missing id.")
|
|
9082
|
+
continue
|
|
9083
|
+
intent_id = intent_id.strip()
|
|
9084
|
+
raw_target = raw_intent.get("automation_target")
|
|
9085
|
+
target = raw_target.strip().lower() if isinstance(raw_target, str) else ""
|
|
9086
|
+
if target == "manual":
|
|
9087
|
+
reason = raw_intent.get("manual_reason_code")
|
|
9088
|
+
if not isinstance(reason, str) or not reason.strip():
|
|
9089
|
+
issues.append(
|
|
9090
|
+
f"qa_policy.manual_reason_code_missing: manual intent {intent_id} must define manual_reason_code."
|
|
9091
|
+
)
|
|
9092
|
+
else:
|
|
9093
|
+
manual_intent_reasons[intent_id] = reason.strip()
|
|
9094
|
+
elif target not in _QA_TEST_LEVELS:
|
|
9095
|
+
issues.append(
|
|
9096
|
+
f"qa_policy.test_level_unclassified: intent {intent_id} has unsupported automation_target {target!r}."
|
|
9097
|
+
)
|
|
9098
|
+
elif not policy["levels"][target]["enabled"]:
|
|
9099
|
+
issues.append(
|
|
9100
|
+
f"qa_policy.level_disabled: intent {intent_id} uses disabled automation_target {target!r}."
|
|
9101
|
+
)
|
|
9102
|
+
else:
|
|
9103
|
+
automated_intent_ids[target].add(intent_id)
|
|
9104
|
+
|
|
9105
|
+
if not isinstance(test_cases_data, dict):
|
|
9106
|
+
return issues + [
|
|
9107
|
+
"qa_policy.test_cases_missing: enforced QA policy requires test-cases.json."
|
|
9108
|
+
]
|
|
9109
|
+
raw_cases = test_cases_data.get("test_cases")
|
|
9110
|
+
if not isinstance(raw_cases, list):
|
|
9111
|
+
return issues + [
|
|
9112
|
+
"qa_policy.test_cases_invalid: test-cases.json test_cases must be an array."
|
|
9113
|
+
]
|
|
9114
|
+
|
|
9115
|
+
covered_automated_intents: dict[str, set[str]] = {
|
|
9116
|
+
level: set() for level in _QA_TEST_LEVELS
|
|
9117
|
+
}
|
|
9118
|
+
covered_manual_intents: set[str] = set()
|
|
9119
|
+
for index, raw_case in enumerate(raw_cases[:200]):
|
|
9120
|
+
if not isinstance(raw_case, dict):
|
|
9121
|
+
issues.append(f"qa_policy.test_case_invalid: test case {index} must be an object.")
|
|
9122
|
+
continue
|
|
9123
|
+
case_id = raw_case.get("id")
|
|
9124
|
+
case_label = str(case_id).strip() if case_id else f"index {index}"
|
|
9125
|
+
raw_type = raw_case.get("test_type")
|
|
9126
|
+
case_type = raw_type.strip().lower() if isinstance(raw_type, str) else ""
|
|
9127
|
+
intent_id = raw_case.get("intent_id") or raw_case.get("test_intent_id")
|
|
9128
|
+
intent_id = intent_id.strip() if isinstance(intent_id, str) else ""
|
|
9129
|
+
|
|
9130
|
+
if case_type == "manual":
|
|
9131
|
+
if intent_id not in manual_intent_reasons:
|
|
9132
|
+
issues.append(
|
|
9133
|
+
f"qa_policy.manual_case_intent_mismatch: manual test case {case_label} must reference a manual intent."
|
|
9134
|
+
)
|
|
9135
|
+
else:
|
|
9136
|
+
covered_manual_intents.add(intent_id)
|
|
9137
|
+
reason = raw_case.get("manual_reason_code")
|
|
9138
|
+
if not isinstance(reason, str) or reason.strip() != manual_intent_reasons[intent_id]:
|
|
9139
|
+
issues.append(
|
|
9140
|
+
f"qa_policy.manual_reason_code_mismatch: manual test case {case_label} must preserve its intent reason."
|
|
9141
|
+
)
|
|
9142
|
+
if not isinstance(raw_case.get("acceptance_criteria_refs"), list) or not raw_case["acceptance_criteria_refs"]:
|
|
9143
|
+
issues.append(
|
|
9144
|
+
f"qa_policy.manual_case_missing_acceptance_refs: manual test case {case_label} must list acceptance_criteria_refs."
|
|
9145
|
+
)
|
|
9146
|
+
if not isinstance(raw_case.get("steps"), list) or not raw_case["steps"]:
|
|
9147
|
+
issues.append(
|
|
9148
|
+
f"qa_policy.manual_case_missing_steps: manual test case {case_label} must define executable steps."
|
|
9149
|
+
)
|
|
9150
|
+
if raw_case.get("script_path") or str(raw_case.get("script_content") or "").strip():
|
|
9151
|
+
issues.append(
|
|
9152
|
+
f"qa_policy.manual_case_has_script: manual test case {case_label} must not define a script."
|
|
9153
|
+
)
|
|
9154
|
+
continue
|
|
9155
|
+
|
|
9156
|
+
if case_type not in _QA_TEST_LEVELS:
|
|
9157
|
+
issues.append(
|
|
9158
|
+
f"qa_policy.test_level_unclassified: test case {case_label} has unsupported test_type {case_type!r}."
|
|
9159
|
+
)
|
|
9160
|
+
continue
|
|
9161
|
+
if not policy["levels"][case_type]["enabled"]:
|
|
9162
|
+
issues.append(
|
|
9163
|
+
f"qa_policy.level_disabled: test case {case_label} uses disabled test_type {case_type!r}."
|
|
9164
|
+
)
|
|
9165
|
+
continue
|
|
9166
|
+
if intent_id:
|
|
9167
|
+
covered_automated_intents[case_type].add(intent_id)
|
|
9168
|
+
|
|
9169
|
+
runner_family = raw_case.get("runner_family")
|
|
9170
|
+
runner = runner_family.strip().lower() if isinstance(runner_family, str) else ""
|
|
9171
|
+
if not runner:
|
|
9172
|
+
issues.append(
|
|
9173
|
+
f"qa_policy.runner_family_missing: automated test case {case_label} must define runner_family."
|
|
9174
|
+
)
|
|
9175
|
+
elif (case_type == "e2e" and runner != "playwright") or (
|
|
9176
|
+
case_type in {"unit", "integration"} and runner == "playwright"
|
|
9177
|
+
):
|
|
9178
|
+
issues.append(
|
|
9179
|
+
f"qa_policy.runner_level_mismatch: test case {case_label} runner_family {runner!r} conflicts with test_type {case_type!r}."
|
|
9180
|
+
)
|
|
9181
|
+
|
|
9182
|
+
script_content = raw_case.get("script_content")
|
|
9183
|
+
if not isinstance(script_content, str) or not script_content.strip():
|
|
9184
|
+
issues.append(
|
|
9185
|
+
f"qa_policy.script_content_missing: automated test case {case_label} must define non-empty script_content."
|
|
9186
|
+
)
|
|
9187
|
+
script_path = raw_case.get("script_path")
|
|
9188
|
+
if not isinstance(script_path, str) or not script_path.strip():
|
|
9189
|
+
issues.append(
|
|
9190
|
+
f"qa_policy.script_path_missing: automated test case {case_label} must define script_path."
|
|
9191
|
+
)
|
|
9192
|
+
else:
|
|
9193
|
+
candidate_path = Path(script_path)
|
|
9194
|
+
if candidate_path.is_absolute() or ".." in candidate_path.parts:
|
|
9195
|
+
issues.append(
|
|
9196
|
+
f"qa_policy.script_path_invalid: automated test case {case_label} script_path must stay inside the workspace."
|
|
9197
|
+
)
|
|
9198
|
+
elif not (workspace_path / candidate_path).is_file():
|
|
9199
|
+
issues.append(
|
|
9200
|
+
f"qa_policy.script_path_missing: automated test case {case_label} script_path '{script_path}' does not exist on disk."
|
|
9201
|
+
)
|
|
9202
|
+
|
|
9203
|
+
for level, intent_ids in automated_intent_ids.items():
|
|
9204
|
+
missing = sorted(intent_ids - covered_automated_intents[level])
|
|
9205
|
+
if missing:
|
|
9206
|
+
issues.append(
|
|
9207
|
+
f"qa_policy.test_case_missing: test-cases.json lacks {level} test cases for intents {missing}."
|
|
9208
|
+
)
|
|
9209
|
+
missing_manual = sorted(set(manual_intent_reasons) - covered_manual_intents)
|
|
9210
|
+
if missing_manual:
|
|
9211
|
+
issues.append(
|
|
9212
|
+
f"qa_policy.manual_case_missing: test-cases.json lacks manual test cases for intents {missing_manual}."
|
|
9213
|
+
)
|
|
9214
|
+
return issues
|
|
9215
|
+
|
|
9216
|
+
|
|
8958
9217
|
def _configured_daemon_api_token() -> str:
|
|
8959
9218
|
token = str(settings.DAEMON_API_TOKEN or "").strip()
|
|
8960
9219
|
if token.startswith("pat_"):
|
|
@@ -10254,7 +10513,7 @@ class RuntimeDaemon:
|
|
|
10254
10513
|
|
|
10255
10514
|
tried_agents.add(agent.agent_id)
|
|
10256
10515
|
|
|
10257
|
-
# ── Agent fallback: try another CLI after a backend failure or a
|
|
10516
|
+
# ── Agent fallback: try another CLI after a backend failure or a work-free hang ──
|
|
10258
10517
|
# Guard: if the agent already produced file changes in the workspace, it DID
|
|
10259
10518
|
# meaningful work — don't trigger fallback even if it crashed after completing.
|
|
10260
10519
|
# Let the recovery logic (step 4.1) handle non-zero exit with committed work.
|
|
@@ -10268,12 +10527,12 @@ class RuntimeDaemon:
|
|
|
10268
10527
|
else ProcessManager.agent_failure_kind(result)
|
|
10269
10528
|
)
|
|
10270
10529
|
is_agent_fallback_eligible = agent_failure_kind is not None
|
|
10271
|
-
|
|
10530
|
+
is_idle_timeout_without_work = self.process_manager.is_idle_timeout_without_work(result)
|
|
10272
10531
|
fallback_reason = (
|
|
10273
10532
|
agent_failure_kind
|
|
10274
10533
|
if is_agent_fallback_eligible
|
|
10275
10534
|
else "agent_idle_timeout"
|
|
10276
|
-
if
|
|
10535
|
+
if is_idle_timeout_without_work
|
|
10277
10536
|
else ""
|
|
10278
10537
|
)
|
|
10279
10538
|
fallback_label = (
|
|
@@ -11396,6 +11655,10 @@ class RuntimeDaemon:
|
|
|
11396
11655
|
|
|
11397
11656
|
if not _skip_test_artifacts:
|
|
11398
11657
|
_input = task.input_data or {}
|
|
11658
|
+
_qa_policy, _qa_policy_issues = _parse_frozen_qa_test_level_policy(
|
|
11659
|
+
_input.get("qa_test_level_policy_snapshot")
|
|
11660
|
+
)
|
|
11661
|
+
issues.extend(_qa_policy_issues)
|
|
11399
11662
|
doc_dir = (
|
|
11400
11663
|
_input.get("output_dir")
|
|
11401
11664
|
or _input.get("context", {}).get("output_dir")
|
|
@@ -11411,6 +11674,8 @@ class RuntimeDaemon:
|
|
|
11411
11674
|
# intents gets the same E2E chain validation as feature/improvement.
|
|
11412
11675
|
_e2e_intent_ids: set[str] = set()
|
|
11413
11676
|
_e2e_case_ids: list[str] = []
|
|
11677
|
+
_ti_data: dict[str, Any] | None = None
|
|
11678
|
+
_tc_data: dict[str, Any] | None = None
|
|
11414
11679
|
_analysis_dir = (
|
|
11415
11680
|
_input.get("analysis_output_dir")
|
|
11416
11681
|
or (_input.get("context") or {}).get("analysis_output_dir")
|
|
@@ -11424,6 +11689,8 @@ class RuntimeDaemon:
|
|
|
11424
11689
|
_ti_path.read_text(encoding="utf-8")
|
|
11425
11690
|
)
|
|
11426
11691
|
for _ti in _ti_data.get("intents", []):
|
|
11692
|
+
if not isinstance(_ti, dict):
|
|
11693
|
+
continue
|
|
11427
11694
|
if (_ti.get("automation_target") or "").lower() == "e2e":
|
|
11428
11695
|
_ti_id = (
|
|
11429
11696
|
_ti.get("id")
|
|
@@ -11445,8 +11712,8 @@ class RuntimeDaemon:
|
|
|
11445
11712
|
tc_path = base / "test-cases.json"
|
|
11446
11713
|
if tc_path.exists():
|
|
11447
11714
|
try:
|
|
11448
|
-
|
|
11449
|
-
cases =
|
|
11715
|
+
_tc_data = _json.loads(tc_path.read_text(encoding="utf-8"))
|
|
11716
|
+
cases = _tc_data.get("test_cases", [])
|
|
11450
11717
|
_e2e_case_ids = [
|
|
11451
11718
|
str(_tc.get("id"))
|
|
11452
11719
|
for _tc in cases
|
|
@@ -11526,6 +11793,14 @@ class RuntimeDaemon:
|
|
|
11526
11793
|
elif _requires_structured_artifacts or _requires_e2e_artifacts:
|
|
11527
11794
|
issues.append(f"test-cases.json not found in {doc_dir or 'workspace root'}")
|
|
11528
11795
|
|
|
11796
|
+
if _qa_policy is not None:
|
|
11797
|
+
issues.extend(_validate_enforced_qa_test_level_artifacts(
|
|
11798
|
+
_qa_policy,
|
|
11799
|
+
_ti_data,
|
|
11800
|
+
_tc_data,
|
|
11801
|
+
workspace_path,
|
|
11802
|
+
))
|
|
11803
|
+
|
|
11529
11804
|
# --- coverage-matrix.json validation ---
|
|
11530
11805
|
cm_path = base / "coverage-matrix.json"
|
|
11531
11806
|
if cm_path.exists():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|