fix(038): QA gate — uicontext None guard in agent handler, ruff compliance, belief-scope wiring
- agent_handler: guard scenario_mode against None uicontext (regression in test_handler_missing_auth_continues_gracefully) - tools_038.py: sorted imports, noqa ARG001 for schema-bound scenario_json - compiler/validator: wrap pure cores in belief_scope for runtime projection - scenario tests: ruff import order and unused-argument fixes in test_capture.py, test_capture_dispatch.py, test_vlm.py Backend scenario 80 passed; dashboard-testing 378 passed; agent 352 passed, 12 skipped; ruff clean for 038 scope.
This commit is contained in:
@@ -559,7 +559,7 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
|
||||
}
|
||||
)
|
||||
# ── 036: Create durable run for scenario intent ──────
|
||||
scenario_mode = uicontext.get("intent") == "build_dashboard_test_scenario"
|
||||
scenario_mode = bool(uicontext and uicontext.get("intent") == "build_dashboard_test_scenario")
|
||||
run_tracker = None
|
||||
if scenario_mode and uicontext:
|
||||
try:
|
||||
|
||||
@@ -12,12 +12,11 @@ from typing import Any
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from pydantic import BaseModel, Field
|
||||
from ss_tools.shared._llm_http import get_shared_http_client
|
||||
from ss_tools.shared.cot_logger import log as _cot_log
|
||||
|
||||
from ss_tools.agent._config import FASTAPI_URL
|
||||
from ss_tools.agent._tool_filter import enforce_tool_permission
|
||||
from ss_tools.agent.context import get_service_jwt, get_user_jwt, get_user_role
|
||||
from ss_tools.shared._llm_http import get_shared_http_client
|
||||
from ss_tools.shared.cot_logger import log as _cot_log
|
||||
|
||||
TOOL_RESPONSE_LIMIT = 12000
|
||||
TOOL_TIMEOUT_SECONDS = 120
|
||||
@@ -155,7 +154,7 @@ async def scenario_validate(scenario_json: str) -> str:
|
||||
# @BRIEF Apply typed resolutions; returns a new immutable revision.
|
||||
@tool(args_schema=ResolveScenarioInput)
|
||||
async def scenario_resolve(
|
||||
scenario_id: str, scenario_json: str,
|
||||
scenario_id: str, scenario_json: str, # noqa: ARG001 — retained for tool schema compatibility
|
||||
base_revision_hash: str, changes_json: str,
|
||||
) -> str:
|
||||
"""Resolve parameters/selectors and emit an immutable revision."""
|
||||
|
||||
@@ -155,6 +155,21 @@ def compile_scenario(req: CompileScenarioRequest) -> CompiledResult:
|
||||
# @POST Same canonical inputs/compiler version yield byte-identical graph and stable ids/order.
|
||||
def _compile_impl(req: CompileScenarioRequest) -> CompiledResult:
|
||||
# Normalize objective: selected_case_ids order is NOT part of identity.
|
||||
log("ScenarioGraph.Compiler.CompileImpl", "REASON", "Compiling deterministic graph",
|
||||
{"dashboard_id": req.dashboard_id})
|
||||
with belief_scope("ScenarioGraph.Compiler.CompileImpl", "Deterministic graph build"):
|
||||
result = _compile_graph(req)
|
||||
log("ScenarioGraph.Compiler.CompileImpl", "REFLECT", "Graph built",
|
||||
{"steps": len(result.scenario.steps)})
|
||||
return result
|
||||
# #endregion ScenarioGraph.Compiler.CompileImpl
|
||||
|
||||
|
||||
# #region ScenarioGraph.Compiler.CompileGraph [C:3] [TYPE Function] [SEMANTICS scenario,compiler,deterministic]
|
||||
# @ingroup ScenarioGraph
|
||||
# @BRIEF Pure graph construction (no logging) — wrapped by CompileImpl.
|
||||
# @POST Same canonical inputs/compiler version yield byte-identical graph and stable ids/order.
|
||||
def _compile_graph(req: CompileScenarioRequest) -> CompiledResult:
|
||||
objective = dict(req.objective)
|
||||
objective["selected_case_ids"] = sorted(set(objective.get("selected_case_ids", [])))
|
||||
|
||||
@@ -226,7 +241,7 @@ def _compile_impl(req: CompileScenarioRequest) -> CompiledResult:
|
||||
log("ScenarioGraph.Compiler.Compile", "REFLECT", "Scenario compiled",
|
||||
payload={"steps": len(steps), "hash": scenario.revision_hash[:16]})
|
||||
return CompiledResult(scenario=scenario, warnings=warnings, blockers=blockers)
|
||||
# #endregion ScenarioGraph.Compiler.CompileImpl
|
||||
# #endregion ScenarioGraph.Compiler.CompileGraph
|
||||
|
||||
|
||||
# #region ScenarioGraph.Compiler.ScenarioId [C:1] [TYPE Function] [SEMANTICS scenario,id,slug]
|
||||
|
||||
@@ -90,7 +90,8 @@ def _detect_sql(text: str | None) -> bool:
|
||||
def _validate_core(scenario: DashboardTestScenario) -> ScenarioValidationResult:
|
||||
log("ScenarioGraph.Validator.ValidateCore", "REASON", "Starting validation checks",
|
||||
{"scenario_id": scenario.scenario_id})
|
||||
result = _validate_checks(scenario)
|
||||
with belief_scope("ScenarioGraph.Validator.ValidateCore", "Validation checks"):
|
||||
result = _validate_checks(scenario)
|
||||
log("ScenarioGraph.Validator.ValidateCore", "REFLECT", "Validation checks complete",
|
||||
{"valid": result.valid, "errors": len(result.errors)})
|
||||
return result
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from src.services.dashboard_testing.scenario.capture_profile import CaptureProfile, load_profile
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from src.services.dashboard_testing.scenario.capture import dispatch_capture
|
||||
from src.services.dashboard_testing.scenario.capture_profile import load_profile
|
||||
@@ -18,12 +17,12 @@ from src.services.dashboard_testing.scenario.capture_profile import load_profile
|
||||
def test_dispatch_without_mask_registers_original_only(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
registered: list[tuple] = []
|
||||
|
||||
def _fake_register(db, run_id, user_id, name, intended_path, sha256, capture_meta, validation_status="pending"):
|
||||
def _fake_register(_db, _run_id, _user_id, name, intended_path, sha256, capture_meta, validation_status="pending"): # noqa: ARG001
|
||||
registered.append(("original", name))
|
||||
return MagicMock(id="art-1")
|
||||
|
||||
monkeypatch.setattr("src.services.dashboard_testing.scenario.capture.register_screenshot_draft", _fake_register)
|
||||
monkeypatch.setattr("src.services.dashboard_testing.scenario.capture.register_masked_derivative", lambda *a, **k: (_ for _ in ()).throw(AssertionError("masked must not be called")))
|
||||
monkeypatch.setattr("src.services.dashboard_testing.scenario.capture.register_masked_derivative", lambda *_a, **_k: (_ for _ in ()).throw(AssertionError("masked must not be called")))
|
||||
|
||||
profile = load_profile("default")
|
||||
profile = profile.model_copy(update={"mask_selectors": []})
|
||||
@@ -36,11 +35,11 @@ def test_dispatch_without_mask_registers_original_only(monkeypatch: pytest.Monke
|
||||
def test_dispatch_with_mask_registers_both(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
registered: list[str] = []
|
||||
|
||||
def _fake_register(db, run_id, user_id, name, intended_path, sha256, capture_meta, validation_status="pending"):
|
||||
def _fake_register(_db, _run_id, _user_id, name, intended_path, sha256, capture_meta, validation_status="pending"): # noqa: ARG001
|
||||
registered.append("original")
|
||||
return MagicMock(id="art-1")
|
||||
|
||||
def _fake_masked(db, run_id, user_id, original_artifact_id, derivative_sha256, intended_path):
|
||||
def _fake_masked(_db, _run_id, _user_id, original_artifact_id, derivative_sha256, intended_path): # noqa: ARG001
|
||||
registered.append("masked")
|
||||
return MagicMock(id="art-2")
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from src.services.dashboard_testing.scenario.models import VlmAnalysis, VlmFinding
|
||||
|
||||
Reference in New Issue
Block a user