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:
2026-07-31 14:22:43 +03:00
parent 610052464d
commit 5719029a71
7 changed files with 29 additions and 13 deletions

View File

@@ -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:

View File

@@ -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."""

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -8,6 +8,7 @@
from __future__ import annotations
import pytest
from pydantic import ValidationError
from src.services.dashboard_testing.scenario.models import VlmAnalysis, VlmFinding