From 67168d3311e3e12adf9a063200acacdbe0bf08aa Mon Sep 17 00:00:00 2001 From: Dawn Jeong Date: Wed, 23 Sep 2026 16:13:17 -0700 Subject: [PATCH 1/5] flightcheck: add canonical DA connection-reference reader + contract test (AB#7852506, AB#7852495, AB#7852511) Extract the shared minimalBots components connection-reference reader into checks/_da_connection_refs.py as the single source that DV-CONN-001 (active agent), ENV-004 (environment-wide, de-duped by logical name) and the Workday shared-parameter checks (WD-ENV-001/WD-REST-001) all read through, so the per-check re-point PRs stop adding divergent copies of it. The module is the superset of PR #317's reader plus a public read_all_agents_connection_references() that de-dupes by connection-reference logical name for ENV-004. Brings the validated/documented mock builders the checks need and a direct pure-logic contract test (18 cases) covering normalization, None-gating (SKIP), fail-loudly ValueError, JSON-string sharedConnectionParameters parsing, and the Workday shared-parameter sweep. Foundation for the 6 DA re-point PRs to rebase onto (single shared reader, no add/add collision). Full flightcheck suite: 1142 passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e25cf992-09c9-49b8-9971-2a19cb2fcb05 --- .../flightcheck/checks/_da_connection_refs.py | 245 ++++++++++++++++++ .../checks/test_da_connection_refs.py | 205 +++++++++++++++ tests/mocks/agentbuilder_connectivity.py | 112 ++++++++ 3 files changed, 562 insertions(+) create mode 100644 solutions/ess-maker-skills/scripts/flightcheck/checks/_da_connection_refs.py create mode 100644 tests/flightcheck/checks/test_da_connection_refs.py diff --git a/solutions/ess-maker-skills/scripts/flightcheck/checks/_da_connection_refs.py b/solutions/ess-maker-skills/scripts/flightcheck/checks/_da_connection_refs.py new file mode 100644 index 000000000..3e40167da --- /dev/null +++ b/solutions/ess-maker-skills/scripts/flightcheck/checks/_da_connection_refs.py @@ -0,0 +1,245 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Canonical reader for Declarative Agent connection references (minimalBots +components API), shared so the DA connection checks cannot drift apart. + +Consumers: + * ``DV-CONN-001`` (checks/workday_extension.py) -> the single active agent's + Workday SOAP reference, via ``read_active_agent_connection_references``. + * ``ENV-004`` (checks/environment.py) -> every configured agent's references, + environment-wide and de-duped by logical name, via + ``read_all_agents_connection_references``. + * The Workday shared-parameter checks (checks/workday.py) -> per-agent Workday + ``sharedConnectionParameters`` via ``workday_shared_connection_parameters``. + +Read shape: ``POST .../components`` -> ``connectionReferenceChanges`` (cassette +``agentbuilder_readiness.yaml``, the same endpoint + shape the shipped native +``DA-CONN-001`` check consumes). + +Fail-loudly contract: + * a missing ``connectionReferenceChanges`` key means genuine absence -> ``[]``; + * a present-but-malformed shape raises ``ValueError`` so the owning check + degrades to a WARNING rather than reporting a confident but wrong verdict; + * a read that cannot be attempted at all (no AgentBuilder client, or no + configured agent botId) returns ``None`` so the caller SKIPs. +""" + +from __future__ import annotations + +import json +from typing import Any + + +WORKDAY_SOAP_CONNECTOR_SUFFIX = "/apis/shared_workdaysoap" + + +def agent_bot_ids(config: dict[str, Any]) -> list[str]: + """Return configured bot IDs from multi-agent and single-agent config.""" + bot_ids: list[str] = [] + for agent in config.get("agents", []) or []: + bid = (agent or {}).get("botId") + if isinstance(bid, str) and bid.strip(): + bot_ids.append(bid.strip()) + single = (config.get("agent") or {}).get("botId") + if isinstance(single, str) and single.strip(): + bot_ids.append(single.strip()) + + seen: set[str] = set() + ordered: list[str] = [] + for bot_id in bot_ids: + folded = bot_id.casefold() + if folded not in seen: + seen.add(folded) + ordered.append(bot_id) + return ordered + + +def _bot_connection_references(client, bot_id: str) -> list[dict[str, Any]]: + """Fetch + normalize one agent's connection references from the minimalBots + components API. + + Raises ``ValueError`` for a malformed ``connectionReferenceChanges`` shape + so the owning check reports a WARNING instead of overclaiming. + """ + changeset = client.fetch_components(bot_id) or {} + changes = changeset.get("connectionReferenceChanges") + if changes is None: + return [] + if not isinstance(changes, list): + raise ValueError( + "Component fetch returned invalid connectionReferenceChanges." + ) + + refs: list[dict[str, Any]] = [] + for change in changes: + item = ( + change.get("connectionReference") + if isinstance(change, dict) + else None + ) + if not isinstance(item, dict): + continue + refs.append( + { + "botid": bot_id, + "connectionreferencelogicalname": item.get( + "connectionReferenceLogicalName" + ), + "connectorid": item.get("connectorId"), + "connectionid": item.get("connectionId"), + "sharedconnectionparameters": item.get( + "sharedConnectionParameters" + ), + } + ) + return refs + + +def read_active_agent_connection_references(runner) -> list[dict[str, Any]] | None: + """The single active agent's DA connection references (config + ``agent.botId``), or ``None`` when the AgentBuilder client or the + active-agent botId is unavailable. + + Used by ``DV-CONN-001`` (checks/workday_extension.py), which validates the + Workday SOAP connection reference on the agent under check. Scoping this to + the active agent — not every configured agent — keeps the check from + reporting on a Workday reference that belongs to a different agent. + Raises ``ValueError`` for malformed components payloads. + """ + client = getattr(runner, "agentbuilder", None) + config = getattr(runner, "config", None) or {} + agent_id = (config.get("agent") or {}).get("botId") + if client is None or not agent_id: + return None + return _bot_connection_references(client, agent_id) + + +def _all_agents_connection_references(runner) -> list[dict[str, Any]] | None: + """Every configured agent's DA connection references (multi-agent and + single-agent config), preserving per-agent rows, or ``None`` when the + AgentBuilder client is unavailable or no agent botId is configured. + + Used by the Workday shared-parameter sweep, which must inspect each + configured agent's own Workday reference rather than only the active one. + Raises ``ValueError`` for malformed components payloads. + """ + client = getattr(runner, "agentbuilder", None) + config = getattr(runner, "config", None) or {} + bot_ids = agent_bot_ids(config) + if client is None or not bot_ids: + return None + + refs: list[dict[str, Any]] = [] + for bot_id in bot_ids: + refs.extend(_bot_connection_references(client, bot_id)) + return refs + + +def read_all_agents_connection_references( + runner, +) -> list[dict[str, Any]] | None: + """Every configured agent's references, de-duped by connection-reference + logical name (first occurrence wins, order preserved), or ``None`` when the + AgentBuilder client is unavailable or no agent botId is configured. + + Used by ``ENV-004`` (checks/environment.py), which is environment-wide + across every agent under check and reports one row per distinct logical + name. The per-agent (non-de-duped) view is ``_all_agents_connection_references``, + which the Workday shared-parameter sweep uses instead. + """ + refs = _all_agents_connection_references(runner) + if refs is None: + return None + seen: set[str] = set() + deduped: list[dict[str, Any]] = [] + for ref in refs: + key = (ref.get("connectionreferencelogicalname") or "").casefold() + if key and key in seen: + continue + if key: + seen.add(key) + deduped.append(ref) + return deduped + + +def _shared_parameter_value(raw_value: Any) -> str: + if isinstance(raw_value, dict): + raw_value = raw_value.get("value") + if raw_value is None: + return "" + return str(raw_value).strip() + + +def shared_connection_parameter_values(ref: dict[str, Any]) -> dict[str, str]: + """Return ``sharedConnectionParameters.values`` as a string map. + + A present-but-malformed shape raises ``ValueError`` because the components + payload no longer matches the validated contract. + """ + params = ref.get("sharedconnectionparameters") + if params is None: + return {} + # Live AgentBuilder returns sharedConnectionParameters as a JSON string, + # not a nested object (observed on a live connection reference), so parse + # the string before validating the shape. + if isinstance(params, str): + text = params.strip() + if not text: + return {} + try: + params = json.loads(text) + except ValueError as exc: + raise ValueError( + "Component fetch returned invalid sharedConnectionParameters." + ) from exc + if not isinstance(params, dict): + raise ValueError( + "Component fetch returned invalid sharedConnectionParameters." + ) + raw_values = params.get("values") + if raw_values is None: + return {} + if not isinstance(raw_values, dict): + raise ValueError( + "Component fetch returned invalid sharedConnectionParameters.values." + ) + return { + str(key): value + for key, raw_value in raw_values.items() + if isinstance(key, str) + if (value := _shared_parameter_value(raw_value)) + } + + +def workday_shared_connection_parameters( + runner, +) -> tuple[dict[str, str] | None, str]: + """Return Workday ``sharedConnectionParameters.values`` from components. + + ``values is None`` means the check could not run because AgentBuilder or a + botId is unavailable. ``values == {}`` means the check ran and observed a + missing Workday reference or missing shared parameters. + """ + refs = _all_agents_connection_references(runner) + if refs is None: + return None, ( + "AgentBuilder client or a configured agent botId not available" + ) + + found_workday_ref = False + for ref in refs: + connector_id = str(ref.get("connectorid") or "").casefold().rstrip("/") + if connector_id.endswith(WORKDAY_SOAP_CONNECTOR_SUFFIX): + found_workday_ref = True + values = shared_connection_parameter_values(ref) + if values: + return values, "" + + if found_workday_ref: + return {}, ( + "Workday connection reference is missing " + "sharedConnectionParameters.values" + ) + + return {}, "Workday connection reference was not found" diff --git a/tests/flightcheck/checks/test_da_connection_refs.py b/tests/flightcheck/checks/test_da_connection_refs.py new file mode 100644 index 000000000..e3b4c42e7 --- /dev/null +++ b/tests/flightcheck/checks/test_da_connection_refs.py @@ -0,0 +1,205 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Contract tests for the canonical Declarative Agent connection-reference +reader (``checks/_da_connection_refs.py``), the single shared module that +``DV-CONN-001`` (active agent), ``ENV-004`` (environment-wide, de-duped) and the +Workday shared-parameter checks all read through. + +These are pure-logic tests: a duck-typed fake AgentBuilder client returns +minimalBots components payloads built from +``tests.mocks.agentbuilder_connectivity`` (``MOCK_STATUS == "validated"``), so +no network replay or cassette is needed (same inline-fake approach as +``test_agent_handoff.py``). Every component shape traces to the validated +``components()`` builder or its documented ``sharedConnectionParameters`` +variant; none is invented here. +""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from flightcheck.checks import _da_connection_refs as reader +from tests.mocks import agentbuilder_connectivity as ab + + +class _FakeClient: + """AgentBuilder stand-in: ``fetch_components(bot_id)`` returns the payload + registered for that bot id (empty ``{}`` when none is registered).""" + + def __init__(self, payload_by_bot: dict[str, dict[str, Any]]): + self._payload_by_bot = payload_by_bot + + def fetch_components(self, bot_id: str) -> dict[str, Any]: + return self._payload_by_bot.get(bot_id, {}) + + +class _FakeRunner: + def __init__(self, client: _FakeClient | None, config: dict[str, Any]): + self.agentbuilder = client + self.config = config + + +# -------------------------------------------------------------------------- +# agent_bot_ids +# -------------------------------------------------------------------------- + +def test_agent_bot_ids_unions_multi_and_single_and_dedups_casefold(): + config = { + "agents": [{"botId": "Agent-A"}, {"botId": "Agent-B"}, {"botId": "agent-a"}], + "agent": {"botId": "Agent-B"}, + } + # agent-a folds onto Agent-A (dup); single Agent-B folds onto Agent-B (dup). + assert reader.agent_bot_ids(config) == ["Agent-A", "Agent-B"] + + +def test_agent_bot_ids_single_only(): + assert reader.agent_bot_ids({"agent": {"botId": "SOLO"}}) == ["SOLO"] + + +def test_agent_bot_ids_ignores_blank_and_non_string(): + config = {"agents": [{"botId": ""}, {"botId": None}, {"botId": " X "}]} + assert reader.agent_bot_ids(config) == ["X"] + + +# -------------------------------------------------------------------------- +# read_active_agent_connection_references (DV-CONN-001 surface) +# -------------------------------------------------------------------------- + +def test_read_active_none_when_no_client(): + runner = _FakeRunner(None, {"agent": {"botId": "BOT"}}) + assert reader.read_active_agent_connection_references(runner) is None + + +def test_read_active_none_when_no_active_bot_id(): + # Only multi-agent config; no config["agent"].botId -> active read SKIPs. + runner = _FakeRunner(_FakeClient({}), {"agents": [{"botId": "BOT"}]}) + assert reader.read_active_agent_connection_references(runner) is None + + +def test_read_active_normalizes_workday_row(): + payload = ab.components_with_references( + references=[ab.workday_connection_reference(connection_id="wd-conn-1")] + ) + runner = _FakeRunner(_FakeClient({"BOT": payload}), {"agent": {"botId": "BOT"}}) + rows = reader.read_active_agent_connection_references(runner) + assert len(rows) == 1 + row = rows[0] + assert row["botid"] == "BOT" + assert row["connectorid"].endswith("/apis/shared_workdaysoap") + assert row["connectionid"] == "wd-conn-1" + + +def test_read_active_missing_change_set_is_empty_not_none(): + runner = _FakeRunner(_FakeClient({"BOT": {}}), {"agent": {"botId": "BOT"}}) + assert reader.read_active_agent_connection_references(runner) == [] + + +def test_read_active_malformed_change_set_raises(): + runner = _FakeRunner( + _FakeClient({"BOT": {"connectionReferenceChanges": "not-a-list"}}), + {"agent": {"botId": "BOT"}}, + ) + with pytest.raises(ValueError): + reader.read_active_agent_connection_references(runner) + + +# -------------------------------------------------------------------------- +# read_all_agents_connection_references (ENV-004 surface: env-wide, de-duped) +# -------------------------------------------------------------------------- + +def test_read_all_none_when_no_client(): + runner = _FakeRunner(None, {"agents": [{"botId": "A"}]}) + assert reader.read_all_agents_connection_references(runner) is None + + +def test_read_all_dedups_by_logical_name_first_wins(): + ref_a = ab.workday_connection_reference( + connection_id="c1", logical_name="ns.shared_workdaysoap" + ) + ref_b = ab.workday_connection_reference( + connection_id="c2", logical_name="ns.shared_workdaysoap" + ) + client = _FakeClient( + { + "A": ab.components_with_references(references=[ref_a]), + "B": ab.components_with_references(references=[ref_b]), + } + ) + runner = _FakeRunner(client, {"agents": [{"botId": "A"}, {"botId": "B"}]}) + rows = reader.read_all_agents_connection_references(runner) + assert len(rows) == 1 + assert rows[0]["connectionid"] == "c1" + + +# -------------------------------------------------------------------------- +# shared_connection_parameter_values +# -------------------------------------------------------------------------- + +def test_scp_values_parsed_from_json_string(): + ref = { + "sharedconnectionparameters": ab.shared_connection_parameters_json_string( + rest_base_uri="https://wd.example.com/ccx/api" + ) + } + values = reader.shared_connection_parameter_values(ref) + assert values["restBaseUri"] == "https://wd.example.com/ccx/api" + + +def test_scp_values_parsed_from_nested_object(): + ref = {"sharedconnectionparameters": ab.shared_connection_parameters(tenant_name="mocktenant")} + values = reader.shared_connection_parameter_values(ref) + assert values["tenantName"] == "mocktenant" + + +def test_scp_values_missing_is_empty_map(): + assert reader.shared_connection_parameter_values({}) == {} + + +def test_scp_values_malformed_json_string_raises(): + ref = {"sharedconnectionparameters": "{not valid json"} + with pytest.raises(ValueError): + reader.shared_connection_parameter_values(ref) + + +# -------------------------------------------------------------------------- +# workday_shared_connection_parameters (WD-ENV-001 / WD-REST-001 surface) +# -------------------------------------------------------------------------- + +def test_wscp_found_with_values(): + ref = ab.workday_connection_reference( + shared_connection_parameters=ab.shared_connection_parameters_json_string() + ) + payload = ab.components_with_references(references=[ref]) + runner = _FakeRunner(_FakeClient({"BOT": payload}), {"agent": {"botId": "BOT"}}) + values, message = reader.workday_shared_connection_parameters(runner) + assert message == "" + assert values["tenantName"] == "mocktenant" + + +def test_wscp_found_but_missing_values(): + ref = ab.workday_connection_reference(shared_connection_parameters=None) + payload = ab.components_with_references(references=[ref]) + runner = _FakeRunner(_FakeClient({"BOT": payload}), {"agent": {"botId": "BOT"}}) + values, message = reader.workday_shared_connection_parameters(runner) + assert values == {} + assert "missing" in message.lower() + + +def test_wscp_workday_reference_not_found(): + # components() carries only the ServiceNow reference, no Workday one. + runner = _FakeRunner( + _FakeClient({"BOT": ab.components()}), {"agent": {"botId": "BOT"}} + ) + values, message = reader.workday_shared_connection_parameters(runner) + assert values == {} + assert "not found" in message.lower() + + +def test_wscp_none_when_client_unavailable(): + runner = _FakeRunner(None, {"agent": {"botId": "BOT"}}) + values, message = reader.workday_shared_connection_parameters(runner) + assert values is None + assert message diff --git a/tests/mocks/agentbuilder_connectivity.py b/tests/mocks/agentbuilder_connectivity.py index 9fb6c123d..f666f928c 100644 --- a/tests/mocks/agentbuilder_connectivity.py +++ b/tests/mocks/agentbuilder_connectivity.py @@ -5,6 +5,7 @@ from __future__ import annotations +import json from typing import Any, Iterable import responses @@ -20,6 +21,7 @@ MOCK_AGENT_ID = "00000000-0000-0000-0000-000000002222" MOCK_FAMILY_ID = "00000000-0000-0000-0000-000000003333" MOCK_CONNECTION_ID = "mock-servicenow-connection" +MOCK_WORKDAY_CONNECTION_ID = "mock-workday-connection" MOCK_AGENTBUILDER_BASE = ( "https://00000000000000000000000000000000." "0.environment.api.test.powerplatform.com" @@ -76,6 +78,116 @@ def components() -> dict[str, Any]: } +def connection_reference_change( + *, + connector: str, + connection_id: str | None, + logical_name: str | None = None, + shared_connection_parameters: dict[str, Any] | None = None, +) -> dict[str, Any]: + """One ``connectionReferenceChanges`` entry in the validated minimalBots + components shape (cassette ``agentbuilder_readiness.yaml``; the same shape + the shipped native ``DA-CONN-001`` check consumes). Only the ``connectorId`` + value varies from the captured ServiceNow reference, so this is + same-endpoint value variance and needs no new cassette (see + ``scripts/flightcheck/AGENTS.md``). ``connection_id=None`` models an unbound + reference. + """ + reference = { + "connectionReferenceLogicalName": ( + logical_name + or f"gptagent_mockemployeeselfservice.{connector}" + ), + "connectorId": ( + f"/providers/Microsoft.PowerApps/apis/{connector}" + ), + "connectionId": connection_id, + } + if shared_connection_parameters is not None: + reference["sharedConnectionParameters"] = ( + shared_connection_parameters + ) + return { + "changeType": "Insert", + "connectionReference": reference, + } + + +def workday_connection_reference( + *, + connection_id: str | None = MOCK_WORKDAY_CONNECTION_ID, + logical_name: str | None = None, + shared_connection_parameters: dict[str, Any] | None = None, +) -> dict[str, Any]: + """The Workday SOAP (``shared_workdaysoap``) connection-reference variant + that ``DV-CONN-001`` filters on.""" + return connection_reference_change( + connector="shared_workdaysoap", + connection_id=connection_id, + logical_name=logical_name, + shared_connection_parameters=shared_connection_parameters, + ) + + +def shared_connection_parameters( + *, + rest_base_uri: str | None = "https://wd.example.com/ccx/api", + tenant_name: str | None = "mocktenant", + resource_uri: str | None = "https://wd.example.com", + token_uri: str | None = "https://wd.example.com/ccx/oauth2/mocktenant/token", + client_id: str | None = "mock-client-id", +) -> dict[str, Any]: + """Workday ``sharedConnectionParameters`` from documented sources. + + Source (documented): + ``tools/ess-ca-to-da/reference/hr/agent.yml`` captures a real + ServiceNow ``sharedConnectionParameters`` entry using the nested + ``values..value`` wrapper shape. The Workday-specific fields mirror + the public Workday connector definition documented at + ``https://learn.microsoft.com/connectors/workdaysoap/``: + ``restBaseUri``, ``tenantName``, ``token:ResourceUri``, + ``token:WorkdayTokenUri``, and ``token:WorkdayClientId``. These + Workday-specific keys are not yet captured from a live AgentBuilder + components response. + """ + values: dict[str, dict[str, str]] = {} + for key, value in ( + ("restBaseUri", rest_base_uri), + ("tenantName", tenant_name), + ("token:ResourceUri", resource_uri), + ("token:WorkdayTokenUri", token_uri), + ("token:WorkdayClientId", client_id), + ): + if value is not None: + values[key] = {"value": value} + return {"values": values} + + +def shared_connection_parameters_json_string(**kwargs: Any) -> str: + """``sharedConnectionParameters`` as the JSON string the live AgentBuilder + components response returns, rather than a nested object. + + Source (documented): a live ServiceNow connection reference encodes + ``sharedConnectionParameters`` as a JSON string, so checks must parse it + before reading ``values``. + """ + return json.dumps(shared_connection_parameters(**kwargs)) + + +def components_with_references( + *, + references: Iterable[dict[str, Any]] | None = None, +) -> dict[str, Any]: + """``components()`` with its ``connectionReferenceChanges`` replaced by the + given references (``None`` -> an empty list, modelling an agent with no + connection references).""" + payload = components() + payload["connectionReferenceChanges"] = ( + [] if references is None else list(references) + ) + return payload + + def connection( *, connection_id: str = MOCK_CONNECTION_ID, From dba6714282a38a20ab2cb831600a4c44edc26cc4 Mon Sep 17 00:00:00 2001 From: Dawn Jeong Date: Tue, 22 Sep 2026 14:50:24 -0700 Subject: [PATCH 2/5] flightcheck: re-point DV-CONN-001 to Declarative Agent components API (7852506) Read the Workday SOAP connection reference from the Declarative Agent minimalBots components API (runner.agentbuilder.fetch_components) instead of the Custom Agent Dataverse connectionreferences query, so DV-CONN-001 works against DA-GA agents that no longer expose Dataverse. Verdict simplifies to found + bound (connectionId present); the Dataverse-only statuscode and multiple-ref branches are dropped since the components shape carries neither. BAP owner echo is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../flightcheck/checks/workday_extension.py | 195 +++++++---------- .../scripts/flightcheck/registry.py | 9 +- .../checks/test_workday_extension.py | 197 ++++++------------ tests/flightcheck/test_registry.py | 12 +- 4 files changed, 149 insertions(+), 264 deletions(-) diff --git a/solutions/ess-maker-skills/scripts/flightcheck/checks/workday_extension.py b/solutions/ess-maker-skills/scripts/flightcheck/checks/workday_extension.py index 9f023bc01..0dc741a17 100644 --- a/solutions/ess-maker-skills/scripts/flightcheck/checks/workday_extension.py +++ b/solutions/ess-maker-skills/scripts/flightcheck/checks/workday_extension.py @@ -20,11 +20,12 @@ (never assert a verdict from an unconfirmed API response shape) — this checkpoint echoes the observed ``connectionParametersSet.name`` for the operator to confirm, rather than PASS/FAIL on a guessed value. - * ``DV-CONN-001`` (S5.4) — the Dataverse connection reference the extension - pack ships (``…_92b66``, connector ``shared_commondataserviceforapps``) is - bound to an **active** connection, and its owner is echoed so the operator - can confirm it is their **own** account. Programmatic PASS/FAIL on a - documented-tier Dataverse ``connectionreferences`` read. + * ``DV-CONN-001`` (S5.4) — the Workday SOAP connection reference reported by + the Declarative Agent minimalBots components API is bound (``connectionId`` + present), and its owner is echoed so the operator can confirm it is their + **own** account. Programmatic PASS/FAIL on the validated minimalBots + components read (same endpoint + ``connectionReferenceChanges`` shape as the + shipped native ``DA-CONN-001`` check). * ``WD-REST-001`` (S5.5) — the captured ``restBaseUrl`` is present and **trimmed to** ``/api``. Pure-config check, no client. * ``WD-REST-002`` (S5.7) — the agent's ``user-context-setup.mcs.yml`` topic @@ -43,7 +44,7 @@ whole run. * **One CheckResult per checkpoint** (principle 7). * **No guessed API shapes** — the two API-backed checks read documented fields - only (Dataverse ``connectionid`` / ``statuscode``; BAP + only (minimalBots ``connectionReferenceChanges`` connector/connection ids; BAP ``connectionParametersSet.name`` / ``createdBy``), and degrade gracefully when a client is unavailable. * **Every** ``CheckResult`` declares ``roles=`` (enforced by @@ -52,18 +53,11 @@ from __future__ import annotations -import os import re -import sys from pathlib import Path from ..runner import CheckResult, Priority, Role, Status -# scripts/auth.py is on sys.path via cli.py at runtime (tests add it too); this -# mirrors checks/environment.py's top-level import so query_all is patchable as -# flightcheck.checks.workday_extension.query_all. -from auth import query_all # noqa: E402 - DOC_BASE = ( "https://learn.microsoft.com/en-us/copilot/microsoft-365/" "employee-self-service" @@ -91,12 +85,9 @@ _WORKDAY_RUNTIME_REF_LOGICAL_NAME = ( "msdyn_sharedworkdaysoap_workdayruntime" ) -# The Dataverse connection reference the simplified pack ships. -_DATAVERSE_CONNECTOR_SUFFIX = "/apis/shared_commondataserviceforapps" -_DATAVERSE_REF_SUFFIX = "92b66" -_DATAVERSE_RUNTIME_REF_LOGICAL_NAME = ( - "msdyn_sharedcommondataserviceforapps_workdayruntime" -) +# The Workday SOAP connection reference the Declarative Agent reports via the +# minimalBots components API (connector ``shared_workdaysoap``). +_WORKDAY_CONNECTOR_SUFFIX = "/apis/shared_workdaysoap" _REF_SUFFIX_RE = re.compile(r"_([0-9a-f]{5})$") # ---- Local user-context topic (WD-REST-002) ---- @@ -108,7 +99,7 @@ "Workday connection authentication type is Microsoft Entra ID Integrated" ) _DV_CONN_DESC = ( - "Dataverse connection reference bound to an active connection you own" + "Workday SOAP connection reference bound to a connection you own" ) _REST_URL_DESC = "Workday REST base URL present and trimmed to '/api'" _REDIRECT_DESC = ( @@ -155,14 +146,6 @@ def _is_workday_auth_ref(logical_name) -> bool: ) -def _is_dataverse_runtime_ref(logical_name) -> bool: - normalized = str(logical_name or "").casefold() - return ( - _ref_suffix(logical_name) == _DATAVERSE_REF_SUFFIX - or normalized == _DATAVERSE_RUNTIME_REF_LOGICAL_NAME.casefold() - ) - - def _host_of(url: str) -> str: """Return the host portion of an ``https://host/…`` URL for display.""" match = re.match(r"https?://([^/]+)", str(url).strip()) @@ -192,26 +175,40 @@ def _resolve_owner(props: dict) -> str: def _query_connection_references(runner): - """Return all Dataverse ``connectionreferences`` rows, or ``None`` when the - Dataverse token/endpoint is not available. - - Documented-tier read (Dataverse Web API v9.2) — no cassette required; tests - stub ``query_all``. + """Return the agent's connection references from the Declarative Agent + minimalBots components API, normalized to the row shape + ``_check_dv_connection`` consumes, or ``None`` when the AgentBuilder client + or the active-agent ``botId`` is unavailable. + + Validated-tier read (minimalBots ``POST …/components``). The same endpoint + and ``connectionReferenceChanges`` shape already back the shipped native + ``DA-CONN-001`` check (``checks/native_agent.py``); see + ``tests/fixtures/cassettes/INDEX.md`` and ``tests/mocks/ + agentbuilder_connectivity.py``. Any ``AgentBuilderHTTPError`` propagates so + the dispatcher degrades this checkpoint to a WARNING (fail loudly). """ - env_url = getattr(runner, "env_url", None) - dv_token = getattr(runner, "dv_token", None) - if not env_url or not dv_token: + client = getattr(runner, "agentbuilder", None) + config = getattr(runner, "config", None) or {} + agent_id = (config.get("agent") or {}).get("botId") + if client is None or not agent_id: return None - # Belt-and-suspenders: keep scripts/ importable even if the module was - # imported before cli.py put it on the path. - sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) - return query_all( - env_url, - dv_token, - "connectionreferences", - "connectionreferenceid,connectionreferencelogicalname," - "connectionreferencedisplayname,connectorid,connectionid,statuscode", - ) + changeset = client.fetch_components(agent_id) or {} + changes = changeset.get("connectionReferenceChanges") + if not isinstance(changes, list): + return [] + refs = [] + for change in changes: + ref = (change or {}).get("connectionReference") or {} + refs.append( + { + "connectionreferencelogicalname": ref.get( + "connectionReferenceLogicalName" + ), + "connectorid": ref.get("connectorId"), + "connectionid": ref.get("connectionId"), + } + ) + return refs def _get_connections(runner): @@ -366,7 +363,7 @@ def _check_connection_auth(runner) -> list[CheckResult]: # ───────────────────────────────────────────────────────────────────── -# DV-CONN-001 — Dataverse connection reference binding (S5.4, PASS/FAIL). +# DV-CONN-001 — Workday SOAP connection reference binding (S5.4, PASS/FAIL). # ───────────────────────────────────────────────────────────────────── @@ -378,67 +375,44 @@ def _check_dv_connection(runner) -> list[CheckResult]: priority=Priority.HIGH.value, status=Status.SKIPPED.value, description=_DV_CONN_DESC, result=( - "Dataverse token not available — skipping the Dataverse " - "connection-reference check." + "AgentBuilder client or active-agent botId not available — " + "skipping the Workday connection-reference check." ), )] - dv_refs = [ - r - for r in refs - if str(r.get("connectorid") or "").lower().endswith( - _DATAVERSE_CONNECTOR_SUFFIX - ) - and _is_dataverse_runtime_ref( - r.get("connectionreferencelogicalname") - ) - ] - if len(dv_refs) > 1: - names = ", ".join( - sorted( - str(ref.get("connectionreferencelogicalname") or "(unnamed)") - for ref in dv_refs - ) - ) - return [CheckResult(roles=_MAKER_ROLES, - checkpoint_id="DV-CONN-001", category=_CATEGORY, - priority=Priority.HIGH.value, status=Status.WARNING.value, - description=_DV_CONN_DESC, - result=( - "Multiple ESS Dataverse connection references match the " - f"runtime and legacy package fingerprints: {names}. " - "FlightCheck cannot determine which reference is active." - ), - remediation=( - "Remove obsolete Workday package references, then rerun " - "FlightCheck against the remaining Dataverse binding." - ), - doc_link=_DOC_SIMPLIFIED, - )] - dv_ref = dv_refs[0] if dv_refs else None + wd_ref = next( + ( + r + for r in refs + if str(r.get("connectorid") or "") + .lower() + .endswith(_WORKDAY_CONNECTOR_SUFFIX) + ), + None, + ) - if dv_ref is None: + if wd_ref is None: return [CheckResult(roles=_MAKER_ROLES, checkpoint_id="DV-CONN-001", category=_CATEGORY, - priority=Priority.HIGH.value, status=Status.NOT_CONFIGURED.value, + priority=Priority.HIGH.value, status=Status.FAILED.value, description=_DV_CONN_DESC, result=( - "The ESS Dataverse connection reference " - f"(\u2026_{_DATAVERSE_REF_SUFFIX}, connector " - "shared_commondataserviceforapps) was not found in this " - "environment." + "The ESS Workday SOAP connection reference (connector " + "shared_workdaysoap) was not found in the Declarative Agent " + "components payload." ), remediation=( - "Install/repair the Workday extension pack so its Dataverse " - "connection reference is created, then bind it to a Dataverse " - "connection you own." + "Install or repair the Workday extension pack so its Workday " + "SOAP connection reference is created, then bind it to a " + "Workday connection you own." ), doc_link=_DOC_SIMPLIFIED, )] - dv_ref_name = str(dv_ref.get("connectionreferencelogicalname")) - connection_id = dv_ref.get("connectionid") - statuscode = dv_ref.get("statuscode") + wd_ref_name = str( + wd_ref.get("connectionreferencelogicalname") or "(unnamed)" + ) + connection_id = wd_ref.get("connectionid") if not connection_id: return [CheckResult(roles=_MAKER_ROLES, @@ -446,31 +420,13 @@ def _check_dv_connection(runner) -> list[CheckResult]: priority=Priority.HIGH.value, status=Status.FAILED.value, description=_DV_CONN_DESC, result=( - "The ESS Dataverse connection reference " - f"({dv_ref_name}) is unbound " - "(connectionid=null)." - ), - remediation=( - "In Power Platform / Copilot Studio, bind the Dataverse " - "connection reference to an active Dataverse connection owned " - "by your own account." - ), - doc_link=_DOC_SIMPLIFIED, - )] - - if statuscode != 1: - return [CheckResult(roles=_MAKER_ROLES, - checkpoint_id="DV-CONN-001", category=_CATEGORY, - priority=Priority.HIGH.value, status=Status.FAILED.value, - description=_DV_CONN_DESC, - result=( - "The ESS Dataverse connection reference " - f"({dv_ref_name}) is bound but inactive " - f"(statuscode={statuscode})." + "The ESS Workday SOAP connection reference " + f"({wd_ref_name}) is unbound (connectionId=null)." ), remediation=( - "Re-authenticate or re-bind the Dataverse connection so its " - "status is active, using an account you own." + "In Power Platform / Copilot Studio, bind the Workday SOAP " + "connection reference to an active Workday connection owned by " + "your own account." ), doc_link=_DOC_SIMPLIFIED, )] @@ -490,9 +446,8 @@ def _check_dv_connection(runner) -> list[CheckResult]: priority=Priority.HIGH.value, status=Status.PASSED.value, description=_DV_CONN_DESC, result=( - "The ESS Dataverse connection reference " - f"({dv_ref_name}) is bound to an active " - "connection." + owner_note + "The ESS Workday SOAP connection reference " + f"({wd_ref_name}) is bound to a connection." + owner_note ), doc_link=_DOC_SIMPLIFIED, )] diff --git a/solutions/ess-maker-skills/scripts/flightcheck/registry.py b/solutions/ess-maker-skills/scripts/flightcheck/registry.py index 6040d49ff..b5badccca 100644 --- a/solutions/ess-maker-skills/scripts/flightcheck/registry.py +++ b/solutions/ess-maker-skills/scripts/flightcheck/registry.py @@ -521,15 +521,16 @@ class ResolvedPlan: priority=Priority.HIGH.value, roles=(Role.ESS_MAKER.value,), ), - # DV-CONN-001 — self-contained Dataverse read (its own connectionreferences - # query) plus a best-effort BAP owner echo. + # DV-CONN-001 — reads the Workday SOAP connection reference from the + # Declarative Agent minimalBots components API (AGENTBUILDER), plus a + # best-effort BAP owner echo (PP_ADMIN). CheckpointSpec( key="DV-CONN-001", category_fn=run_workday_extension_checks, category_label="Workday Extension", - clients=frozenset({DATAVERSE, PP_ADMIN}), + clients=frozenset({AGENTBUILDER, PP_ADMIN}), requires_config=True, - requires_dataverse_endpoint=True, + requires_dataverse_endpoint=False, priority=Priority.HIGH.value, roles=(Role.ESS_MAKER.value,), ), diff --git a/tests/flightcheck/checks/test_workday_extension.py b/tests/flightcheck/checks/test_workday_extension.py index 0e641716b..2910bb762 100644 --- a/tests/flightcheck/checks/test_workday_extension.py +++ b/tests/flightcheck/checks/test_workday_extension.py @@ -10,9 +10,9 @@ connection, degrades gracefully when it does not. Cached-ref read + a best-effort Power Platform admin owner echo — no cassette required (the admin connections listing is the ``validated`` pp_admin mock). - * DV-CONN-001 — PASS/FAIL/NOT_CONFIGURED/SKIPPED over a documented-tier - Dataverse ``connectionreferences`` read (stubbed with ``responses``); owner - echo via the ``validated`` pp_admin mock. + * DV-CONN-001 — PASS/FAIL/SKIPPED over the validated minimalBots components + read (Workday SOAP connection reference; faked ``runner.agentbuilder``); + owner echo via the ``validated`` pp_admin mock. * WD-REST-001 — pure-config check (restBaseUrl trimmed to '/api'). * WD-REST-002 — pure local-file check (user-context redirect topic); SKIPPED on the legacy install path. @@ -28,22 +28,18 @@ from dataclasses import dataclass, field from typing import Any -import responses - from tests.conftest import require_validated_mock +from tests.mocks import agentbuilder_connectivity as ab from tests.mocks import dataverse as dv from tests.mocks import pp_admin as pp +require_validated_mock(ab) require_validated_mock(dv) require_validated_mock(pp) from flightcheck.checks import workday_extension as wx # noqa: E402 from flightcheck.runner import Priority, Role, Status # noqa: E402 -_DV_CONNECTOR_ID = ( - "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps" -) - # ───────────────────────────────────────────────────────────────────── # Minimal runner. The emitters read only these attributes; anything the @@ -62,12 +58,24 @@ def get_connections(self, _env_id: str): return self._connections +class _FakeAgentBuilder: + """Stand-in for FlightCheckRunner.agentbuilder. Only ``fetch_components`` + is consumed (DV-CONN-001's connection-reference read).""" + + def __init__(self, components: dict[str, Any]): + self._components = components + + def fetch_components(self, _agent_id: str): + return self._components + + @dataclass class _Runner: config: Any = field(default_factory=dict) env_url: str | None = None dv_token: str | None = None pp_admin: Any = None + agentbuilder: Any = None env_id: str | None = None _workday_connection_refs: list[dict[str, Any]] = field(default_factory=list) @@ -88,28 +96,6 @@ def _by_id(results): return {r.checkpoint_id: r for r in results} -def _dv_ref(*, connection_id, statuscode=1): - """A Dataverse connection reference matching the extension pack's shipped - ref (connector shared_commondataserviceforapps, logical-name suffix - 92b66).""" - return dv.connection_ref( - logical_name="msdyn_sharedcommondataserviceforapps_92b66", - display_name="Microsoft Dataverse", - connector_id=_DV_CONNECTOR_ID, - connection_id=connection_id, - statuscode=statuscode, - ) - - -def _register_refs(base_url: str, refs: list[dict[str, Any]]) -> None: - responses.add( - method="GET", - url=f"{base_url}/api/data/v9.2/connectionreferences", - json=dv.collection(refs), - status=200, - ) - - # ───────────────────────────────────────────────────────────────────── # WD-CONN-AUTH-001 — always MANUAL echo (S5.3). # ───────────────────────────────────────────────────────────────────── @@ -253,147 +239,88 @@ def test_never_passes_regardless_of_state(self): # ───────────────────────────────────────────────────────────────────── -# DV-CONN-001 — Dataverse connection binding (S5.4, PASS/FAIL). +# DV-CONN-001 — Workday SOAP connection binding (S5.4, PASS/FAIL). # ───────────────────────────────────────────────────────────────────── +def _runner_with_refs(references, *, pp_admin=None, env_id=None): + """A runner whose faked ``agentbuilder.fetch_components`` returns the given + connection references and whose config names an active agent (botId).""" + components = ab.components_with_references(references=references) + return _Runner( + config={"agent": {"botId": ab.MOCK_AGENT_ID}}, + agentbuilder=_FakeAgentBuilder(components), + pp_admin=pp_admin, + env_id=env_id, + ) + + class TestDataverseConnection: - @responses.activate - def test_bound_active_with_owner_echo_passes( - self, fake_dataverse_url, fake_token - ): - _register_refs( - fake_dataverse_url, - [_dv_ref(connection_id="dv-conn-active", statuscode=1)], - ) + def test_bound_with_owner_echo_passes(self): owner_conn = pp.connection( - name="dv-conn-active", - api_name="shared_commondataserviceforapps", + name="wd-conn-active", + api_name="shared_workdaysoap", extra_properties={"accountName": "maker@contoso.com"}, ) - runner = _Runner( - env_url=fake_dataverse_url, - dv_token=fake_token, + runner = _runner_with_refs( + [ab.workday_connection_reference(connection_id="wd-conn-active")], pp_admin=_FakePPAdmin([owner_conn]), env_id="env-1", ) r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] assert r.status == Status.PASSED.value - assert "bound to an active" in r.result + assert "bound to a connection" in r.result assert "maker@contoso.com" in r.result assert "your own account" in r.result - @responses.activate - def test_passes_without_pp_admin_notes_owner_unreadable( - self, fake_dataverse_url, fake_token - ): - _register_refs( - fake_dataverse_url, - [_dv_ref(connection_id="dv-conn-active", statuscode=1)], + def test_passes_without_pp_admin_notes_owner_unreadable(self): + runner = _runner_with_refs( + [ab.workday_connection_reference(connection_id="wd-conn-active")], ) - runner = _Runner(env_url=fake_dataverse_url, dv_token=fake_token) r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] assert r.status == Status.PASSED.value assert "owner could not be read" in r.result assert "your own account" in r.result - @responses.activate - def test_runtime_dataverse_reference_passes( - self, fake_dataverse_url, fake_token - ): - runtime_ref = dv.workday_connection_refs_runtime()[1] - _register_refs(fake_dataverse_url, [runtime_ref]) - runner = _Runner( - env_url=fake_dataverse_url, - dv_token=fake_token, - ) - - r = _by_id( - wx.run_workday_extension_checks(runner) - )["DV-CONN-001"] - - assert r.status == Status.PASSED.value - assert ( - "msdyn_sharedcommondataserviceforapps_workdayruntime" - in r.result + def test_unbound_fails(self): + runner = _runner_with_refs( + [ab.workday_connection_reference(connection_id=None)], ) - - @responses.activate - def test_mixed_runtime_and_legacy_dataverse_refs_warn( - self, fake_dataverse_url, fake_token - ): - runtime_ref = dv.workday_connection_refs_runtime()[1] - _register_refs( - fake_dataverse_url, - [_dv_ref(connection_id="legacy-dv"), runtime_ref], - ) - runner = _Runner( - env_url=fake_dataverse_url, - dv_token=fake_token, - ) - - r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] - - assert r.status == Status.WARNING.value - assert "Multiple ESS Dataverse connection references" in r.result - assert "Remove obsolete Workday package references" in r.remediation - - @responses.activate - def test_unbound_fails(self, fake_dataverse_url, fake_token): - _register_refs( - fake_dataverse_url, [_dv_ref(connection_id=None, statuscode=1)] - ) - runner = _Runner(env_url=fake_dataverse_url, dv_token=fake_token) r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] assert r.status == Status.FAILED.value assert "unbound" in r.result - assert "connectionid=null" in r.result - assert "bind the Dataverse connection reference" in r.remediation - - @responses.activate - def test_inactive_statuscode_fails(self, fake_dataverse_url, fake_token): - _register_refs( - fake_dataverse_url, - [_dv_ref(connection_id="dv-conn-inactive", statuscode=2)], - ) - runner = _Runner(env_url=fake_dataverse_url, dv_token=fake_token) + assert "connectionId=null" in r.result + assert "bind the Workday SOAP connection reference" in r.remediation + + def test_workday_ref_absent_fails(self): + # Only the default ServiceNow ref present — no Workday SOAP ref. + runner = _runner_with_refs(None) r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] assert r.status == Status.FAILED.value - assert "inactive" in r.result - assert "statuscode=2" in r.result - assert "Re-authenticate or re-bind" in r.remediation - - @responses.activate - def test_missing_ref_not_configured(self, fake_dataverse_url, fake_token): - # Only a Workday ref present — no Dataverse (92b66) ref. - _register_refs( - fake_dataverse_url, - [ - dv.connection_ref( - logical_name="new_sharedworkdaysoap_ff0df", - display_name="OAuthUser", - connector_id=dv.WORKDAY_SOAP_CONNECTOR_ID, - connection_id="wd-conn-1", - ) - ], - ) - runner = _Runner(env_url=fake_dataverse_url, dv_token=fake_token) + assert "was not found" in r.result + assert "shared_workdaysoap" in r.result + assert "Install or repair the Workday extension pack" in r.remediation + + def test_no_agentbuilder_client_skips(self): + runner = _Runner(config={"agent": {"botId": ab.MOCK_AGENT_ID}}) r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] - assert r.status == Status.NOT_CONFIGURED.value - assert "was not found in this environment" in r.result - assert "Install/repair the Workday extension pack" in r.remediation + assert r.status == Status.SKIPPED.value + assert "not available" in r.result - def test_no_dv_token_skips(self): - runner = _Runner(env_url="https://x.crm.dynamics.com", dv_token="") + def test_no_active_agent_botid_skips(self): + runner = _Runner( + config={}, + agentbuilder=_FakeAgentBuilder(ab.components_with_references()), + ) r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] assert r.status == Status.SKIPPED.value - assert "Dataverse token not available" in r.result + assert "not available" in r.result # ───────────────────────────────────────────────────────────────────── diff --git a/tests/flightcheck/test_registry.py b/tests/flightcheck/test_registry.py index e8882af57..ca696abe2 100644 --- a/tests/flightcheck/test_registry.py +++ b/tests/flightcheck/test_registry.py @@ -294,7 +294,7 @@ class TestWorkdayExtensionCheckpoints: """skill-5 mints five checkpoints, all sharing checks/workday_extension.run_workday_extension_checks, category "Workday Extension". Two are always-MANUAL echoes/attestations, three are - programmatic (one Dataverse read + two pure-local).""" + programmatic (one minimalBots components read + two pure-local).""" _ALL = ( "WD-CONN-AUTH-001", @@ -329,10 +329,12 @@ def test_conn_auth_exact_beats_wd_conn_family(self): assert registry.resolve("WD-CONN-AUTH-001").key == "WD-CONN-AUTH-001" assert registry.resolve("WD-CONN-AUTH-001").is_family is False - def test_dv_conn_spec_declares_dataverse_and_pp_admin(self): + def test_dv_conn_spec_declares_agentbuilder_and_pp_admin(self): spec = registry.resolve("DV-CONN-001") - assert spec.clients == frozenset({registry.DATAVERSE, registry.PP_ADMIN}) - assert spec.requires_dataverse_endpoint is True + assert spec.clients == frozenset( + {registry.AGENTBUILDER, registry.PP_ADMIN} + ) + assert spec.requires_dataverse_endpoint is False assert spec.prereqs == () assert Role.ESS_MAKER.value in spec.roles @@ -354,7 +356,7 @@ def test_net_check_is_clientless_and_ppadmin_gated(self): def test_dv_conn_plan_unions_clients(self): plan = registry.transitive_requirements("DV-CONN-001") - assert registry.DATAVERSE in plan.clients + assert registry.AGENTBUILDER in plan.clients assert registry.PP_ADMIN in plan.clients def test_all_five_are_listable(self): From 671de3677b9f684b4d36259e47d9af34aaa43226 Mon Sep 17 00:00:00 2001 From: Dawn Jeong Date: Tue, 22 Sep 2026 15:07:25 -0700 Subject: [PATCH 3/5] flightcheck: fail loudly on malformed DV-CONN-001 changeset (review fix) Distinguish a genuinely-absent connectionReferenceChanges (missing key -> no references -> FAILED not-found) from a present-but-non-list payload (a shape we do not understand -> raise ValueError so the dispatcher degrades DV-CONN-001 to a WARNING). Mirrors native_agent._connection_references, the shipped precedent this check re-uses; the prior code collapsed both to [] and could report a confident "reference not found" on an unparseable 200 response. Adds a malformed-changeset test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../flightcheck/checks/workday_extension.py | 14 +++++++++++--- .../flightcheck/checks/test_workday_extension.py | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/solutions/ess-maker-skills/scripts/flightcheck/checks/workday_extension.py b/solutions/ess-maker-skills/scripts/flightcheck/checks/workday_extension.py index 0dc741a17..8ac4a47b1 100644 --- a/solutions/ess-maker-skills/scripts/flightcheck/checks/workday_extension.py +++ b/solutions/ess-maker-skills/scripts/flightcheck/checks/workday_extension.py @@ -184,8 +184,12 @@ def _query_connection_references(runner): and ``connectionReferenceChanges`` shape already back the shipped native ``DA-CONN-001`` check (``checks/native_agent.py``); see ``tests/fixtures/cassettes/INDEX.md`` and ``tests/mocks/ - agentbuilder_connectivity.py``. Any ``AgentBuilderHTTPError`` propagates so - the dispatcher degrades this checkpoint to a WARNING (fail loudly). + agentbuilder_connectivity.py``. Fails loudly (lets the dispatcher degrade + this checkpoint to a WARNING) rather than overclaiming: an + ``AgentBuilderHTTPError`` propagates, and a 200 payload whose + ``connectionReferenceChanges`` is present but not a list raises + ``ValueError`` (mirrors ``native_agent._connection_references``). A missing + changeset is treated as "no references" (genuine absence), not an error. """ client = getattr(runner, "agentbuilder", None) config = getattr(runner, "config", None) or {} @@ -194,8 +198,12 @@ def _query_connection_references(runner): return None changeset = client.fetch_components(agent_id) or {} changes = changeset.get("connectionReferenceChanges") - if not isinstance(changes, list): + if changes is None: return [] + if not isinstance(changes, list): + raise ValueError( + "Component fetch returned invalid connectionReferenceChanges." + ) refs = [] for change in changes: ref = (change or {}).get("connectionReference") or {} diff --git a/tests/flightcheck/checks/test_workday_extension.py b/tests/flightcheck/checks/test_workday_extension.py index 2910bb762..202372eb5 100644 --- a/tests/flightcheck/checks/test_workday_extension.py +++ b/tests/flightcheck/checks/test_workday_extension.py @@ -322,6 +322,22 @@ def test_no_active_agent_botid_skips(self): assert r.status == Status.SKIPPED.value assert "not available" in r.result + def test_malformed_changeset_degrades_to_warning(self): + # A 200 payload whose connectionReferenceChanges is present but not a + # list is a shape we do not understand: fail loudly (dispatcher WARNING) + # rather than reporting a confident "reference not found" FAILED. + runner = _Runner( + config={"agent": {"botId": ab.MOCK_AGENT_ID}}, + agentbuilder=_FakeAgentBuilder( + {"connectionReferenceChanges": {"unexpected": "dict"}} + ), + ) + r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] + + assert r.status == Status.WARNING.value + assert "Unable to run DV-CONN-001" in r.result + assert "DV-CONN-001" in r.remediation + # ───────────────────────────────────────────────────────────────────── # WD-REST-001 — REST base URL trimmed to /api (S5.5). From ab398f13739619cce437148ffa78ac1eab056061 Mon Sep 17 00:00:00 2001 From: Dawn Jeong Date: Tue, 22 Sep 2026 23:20:14 -0700 Subject: [PATCH 4/5] tests: cover DV-CONN-001 ServiceNow-present true-negative (was empty-list) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8246cd2c-37d0-4000-8fe3-fa9b082669e0 --- tests/flightcheck/checks/test_workday_extension.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/flightcheck/checks/test_workday_extension.py b/tests/flightcheck/checks/test_workday_extension.py index 202372eb5..3a4b2dfa1 100644 --- a/tests/flightcheck/checks/test_workday_extension.py +++ b/tests/flightcheck/checks/test_workday_extension.py @@ -296,8 +296,15 @@ def test_unbound_fails(self): assert "bind the Workday SOAP connection reference" in r.remediation def test_workday_ref_absent_fails(self): - # Only the default ServiceNow ref present — no Workday SOAP ref. - runner = _runner_with_refs(None) + # Only a ServiceNow ref is present - no Workday SOAP ref. + runner = _runner_with_refs( + [ + ab.connection_reference_change( + connector="shared_service-now", + connection_id="sn-1", + ) + ] + ) r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] assert r.status == Status.FAILED.value From 35b44cd37156fdf8065d720fdba6dc4a09e8286c Mon Sep 17 00:00:00 2001 From: Dawn Jeong Date: Fri, 25 Sep 2026 12:12:52 -0700 Subject: [PATCH 5/5] flightcheck: fail loud on malformed DA connection-ref payloads (Nkem #304 review nit) Resolves the PR #304 blocker by restoring DV-CONN-001 to its original Dataverse meaning (it now equals main) and dropping the wrong-layer DA Workday SOAP connection check that #304 had bolted onto that ID. The Workday shared_workdaysoap reference is flow/solution-scoped and never surfaces in the DA bot-components (connectionReferenceChanges) layer that the dropped check read, so it would have FAILED "not found" on every real DA GA agent -- the same wrong-layer defect that closed #318 and #328. The correct- layer DA Workday-connection check is deferred to follow-up work. Kept: harden _bot_connection_references to raise on a genuinely malformed connectionReferenceChanges shape instead of silently skipping it. Its live consumer, ENV-004, wraps the read in except Exception and degrades to a warning row, so this is fail-loud without a regression. Absent/null connectionReference entries (non-connection changes) are still tolerated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d47b6fbe-fc1f-402d-aadb-df1a6695da56 --- .../flightcheck/checks/_da_connection_refs.py | 25 +- .../flightcheck/checks/workday_extension.py | 203 ++++++++++------- .../scripts/flightcheck/registry.py | 9 +- .../checks/test_da_connection_refs.py | 35 +++ .../checks/test_workday_extension.py | 214 +++++++++++------- tests/flightcheck/test_registry.py | 12 +- 6 files changed, 315 insertions(+), 183 deletions(-) diff --git a/solutions/ess-maker-skills/scripts/flightcheck/checks/_da_connection_refs.py b/solutions/ess-maker-skills/scripts/flightcheck/checks/_da_connection_refs.py index 3e40167da..0c6d38ca3 100644 --- a/solutions/ess-maker-skills/scripts/flightcheck/checks/_da_connection_refs.py +++ b/solutions/ess-maker-skills/scripts/flightcheck/checks/_da_connection_refs.py @@ -73,13 +73,26 @@ def _bot_connection_references(client, bot_id: str) -> list[dict[str, Any]]: refs: list[dict[str, Any]] = [] for change in changes: - item = ( - change.get("connectionReference") - if isinstance(change, dict) - else None - ) - if not isinstance(item, dict): + # Surface a malformed individual entry instead of silently skipping it + # (PR #304 review): a non-dict change, or a ``connectionReference`` that + # is present but not an object, no longer matches the validated + # contract, so raise and let the owning check degrade to a WARNING. An + # absent or null ``connectionReference`` is tolerated (a non-connection + # change) and skipped. + if not isinstance(change, dict): + raise ValueError( + "Component fetch returned a malformed " + "connectionReferenceChanges entry." + ) + if "connectionReference" not in change: continue + item = change.get("connectionReference") + if item is None: + continue + if not isinstance(item, dict): + raise ValueError( + "Component fetch returned a malformed connectionReference entry." + ) refs.append( { "botid": bot_id, diff --git a/solutions/ess-maker-skills/scripts/flightcheck/checks/workday_extension.py b/solutions/ess-maker-skills/scripts/flightcheck/checks/workday_extension.py index e5bcc81d6..12746ce56 100644 --- a/solutions/ess-maker-skills/scripts/flightcheck/checks/workday_extension.py +++ b/solutions/ess-maker-skills/scripts/flightcheck/checks/workday_extension.py @@ -20,12 +20,11 @@ (never assert a verdict from an unconfirmed API response shape) — this checkpoint echoes the observed ``connectionParametersSet.name`` for the operator to confirm, rather than PASS/FAIL on a guessed value. - * ``DV-CONN-001`` (S5.4) — the Workday SOAP connection reference reported by - the Declarative Agent minimalBots components API is bound (``connectionId`` - present), and its owner is echoed so the operator can confirm it is their - **own** account. Programmatic PASS/FAIL on the validated minimalBots - components read (same endpoint + ``connectionReferenceChanges`` shape as the - shipped native ``DA-CONN-001`` check). + * ``DV-CONN-001`` (S5.4) — the Dataverse connection reference the extension + pack ships (``…_92b66``, connector ``shared_commondataserviceforapps``) is + bound to an **active** connection, and its owner is echoed so the operator + can confirm it is their **own** account. Programmatic PASS/FAIL on a + documented-tier Dataverse ``connectionreferences`` read. * ``WD-REST-001`` (S5.5) — the captured ``restBaseUrl`` is present and **trimmed to** ``/api``. Pure-config check, no client. * ``WD-REST-002`` (S5.7) — the agent's ``user-context-setup.mcs.yml`` topic @@ -44,7 +43,7 @@ whole run. * **One CheckResult per checkpoint** (principle 7). * **No guessed API shapes** — the two API-backed checks read documented fields - only (minimalBots ``connectionReferenceChanges`` connector/connection ids; BAP + only (Dataverse ``connectionid`` / ``statuscode``; BAP ``connectionParametersSet.name`` / ``createdBy``), and degrade gracefully when a client is unavailable. * **Every** ``CheckResult`` declares ``roles=`` (enforced by @@ -53,12 +52,19 @@ from __future__ import annotations +import os import re +import sys from pathlib import Path from ..runner import CheckResult, Priority, Role, Status from ..agent_scope import resolve_agent_directory, validate_agent_slug +# scripts/auth.py is on sys.path via cli.py at runtime (tests add it too); this +# mirrors checks/environment.py's top-level import so query_all is patchable as +# flightcheck.checks.workday_extension.query_all. +from auth import query_all # noqa: E402 + DOC_BASE = ( "https://learn.microsoft.com/en-us/copilot/microsoft-365/" "employee-self-service" @@ -86,9 +92,12 @@ _WORKDAY_RUNTIME_REF_LOGICAL_NAME = ( "msdyn_sharedworkdaysoap_workdayruntime" ) -# The Workday SOAP connection reference the Declarative Agent reports via the -# minimalBots components API (connector ``shared_workdaysoap``). -_WORKDAY_CONNECTOR_SUFFIX = "/apis/shared_workdaysoap" +# The Dataverse connection reference the simplified pack ships. +_DATAVERSE_CONNECTOR_SUFFIX = "/apis/shared_commondataserviceforapps" +_DATAVERSE_REF_SUFFIX = "92b66" +_DATAVERSE_RUNTIME_REF_LOGICAL_NAME = ( + "msdyn_sharedcommondataserviceforapps_workdayruntime" +) _REF_SUFFIX_RE = re.compile(r"_([0-9a-f]{5})$") # ---- Local user-context topic (WD-REST-002) ---- @@ -101,7 +110,7 @@ "Workday connection authentication type is Microsoft Entra ID Integrated" ) _DV_CONN_DESC = ( - "Workday SOAP connection reference bound to a connection you own" + "Dataverse connection reference bound to an active connection you own" ) _REST_URL_DESC = "Workday REST base URL present and trimmed to '/api'" _REDIRECT_DESC = ( @@ -185,6 +194,14 @@ def _is_workday_auth_ref(logical_name) -> bool: ) +def _is_dataverse_runtime_ref(logical_name) -> bool: + normalized = str(logical_name or "").casefold() + return ( + _ref_suffix(logical_name) == _DATAVERSE_REF_SUFFIX + or normalized == _DATAVERSE_RUNTIME_REF_LOGICAL_NAME.casefold() + ) + + def _host_of(url: str) -> str: """Return the host portion of an ``https://host/…`` URL for display.""" match = re.match(r"https?://([^/]+)", str(url).strip()) @@ -214,48 +231,26 @@ def _resolve_owner(props: dict) -> str: def _query_connection_references(runner): - """Return the agent's connection references from the Declarative Agent - minimalBots components API, normalized to the row shape - ``_check_dv_connection`` consumes, or ``None`` when the AgentBuilder client - or the active-agent ``botId`` is unavailable. - - Validated-tier read (minimalBots ``POST …/components``). The same endpoint - and ``connectionReferenceChanges`` shape already back the shipped native - ``DA-CONN-001`` check (``checks/native_agent.py``); see - ``tests/fixtures/cassettes/INDEX.md`` and ``tests/mocks/ - agentbuilder_connectivity.py``. Fails loudly (lets the dispatcher degrade - this checkpoint to a WARNING) rather than overclaiming: an - ``AgentBuilderHTTPError`` propagates, and a 200 payload whose - ``connectionReferenceChanges`` is present but not a list raises - ``ValueError`` (mirrors ``native_agent._connection_references``). A missing - changeset is treated as "no references" (genuine absence), not an error. + """Return all Dataverse ``connectionreferences`` rows, or ``None`` when the + Dataverse token/endpoint is not available. + + Documented-tier read (Dataverse Web API v9.2) — no cassette required; tests + stub ``query_all``. """ - client = getattr(runner, "agentbuilder", None) - config = getattr(runner, "config", None) or {} - agent_id = (config.get("agent") or {}).get("botId") - if client is None or not agent_id: + env_url = getattr(runner, "env_url", None) + dv_token = getattr(runner, "dv_token", None) + if not env_url or not dv_token: return None - changeset = client.fetch_components(agent_id) or {} - changes = changeset.get("connectionReferenceChanges") - if changes is None: - return [] - if not isinstance(changes, list): - raise ValueError( - "Component fetch returned invalid connectionReferenceChanges." - ) - refs = [] - for change in changes: - ref = (change or {}).get("connectionReference") or {} - refs.append( - { - "connectionreferencelogicalname": ref.get( - "connectionReferenceLogicalName" - ), - "connectorid": ref.get("connectorId"), - "connectionid": ref.get("connectionId"), - } - ) - return refs + # Belt-and-suspenders: keep scripts/ importable even if the module was + # imported before cli.py put it on the path. + sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) + return query_all( + env_url, + dv_token, + "connectionreferences", + "connectionreferenceid,connectionreferencelogicalname," + "connectionreferencedisplayname,connectorid,connectionid,statuscode", + ) def _get_connections(runner): @@ -410,7 +405,7 @@ def _check_connection_auth(runner) -> list[CheckResult]: # ───────────────────────────────────────────────────────────────────── -# DV-CONN-001 — Workday SOAP connection reference binding (S5.4, PASS/FAIL). +# DV-CONN-001 — Dataverse connection reference binding (S5.4, PASS/FAIL). # ───────────────────────────────────────────────────────────────────── @@ -422,44 +417,67 @@ def _check_dv_connection(runner) -> list[CheckResult]: priority=Priority.HIGH.value, status=Status.SKIPPED.value, description=_DV_CONN_DESC, result=( - "AgentBuilder client or active-agent botId not available — " - "skipping the Workday connection-reference check." + "Dataverse token not available — skipping the Dataverse " + "connection-reference check." ), )] - wd_ref = next( - ( - r - for r in refs - if str(r.get("connectorid") or "") - .lower() - .endswith(_WORKDAY_CONNECTOR_SUFFIX) - ), - None, - ) + dv_refs = [ + r + for r in refs + if str(r.get("connectorid") or "").lower().endswith( + _DATAVERSE_CONNECTOR_SUFFIX + ) + and _is_dataverse_runtime_ref( + r.get("connectionreferencelogicalname") + ) + ] + if len(dv_refs) > 1: + names = ", ".join( + sorted( + str(ref.get("connectionreferencelogicalname") or "(unnamed)") + for ref in dv_refs + ) + ) + return [CheckResult(roles=_MAKER_ROLES, + checkpoint_id="DV-CONN-001", category=_CATEGORY, + priority=Priority.HIGH.value, status=Status.WARNING.value, + description=_DV_CONN_DESC, + result=( + "Multiple ESS Dataverse connection references match the " + f"runtime and legacy package fingerprints: {names}. " + "FlightCheck cannot determine which reference is active." + ), + remediation=( + "Remove obsolete Workday package references, then rerun " + "FlightCheck against the remaining Dataverse binding." + ), + doc_link=_DOC_SIMPLIFIED, + )] + dv_ref = dv_refs[0] if dv_refs else None - if wd_ref is None: + if dv_ref is None: return [CheckResult(roles=_MAKER_ROLES, checkpoint_id="DV-CONN-001", category=_CATEGORY, - priority=Priority.HIGH.value, status=Status.FAILED.value, + priority=Priority.HIGH.value, status=Status.NOT_CONFIGURED.value, description=_DV_CONN_DESC, result=( - "The ESS Workday SOAP connection reference (connector " - "shared_workdaysoap) was not found in the Declarative Agent " - "components payload." + "The ESS Dataverse connection reference " + f"(\u2026_{_DATAVERSE_REF_SUFFIX}, connector " + "shared_commondataserviceforapps) was not found in this " + "environment." ), remediation=( - "Install or repair the Workday extension pack so its Workday " - "SOAP connection reference is created, then bind it to a " - "Workday connection you own." + "Install/repair the Workday extension pack so its Dataverse " + "connection reference is created, then bind it to a Dataverse " + "connection you own." ), doc_link=_DOC_SIMPLIFIED, )] - wd_ref_name = str( - wd_ref.get("connectionreferencelogicalname") or "(unnamed)" - ) - connection_id = wd_ref.get("connectionid") + dv_ref_name = str(dv_ref.get("connectionreferencelogicalname")) + connection_id = dv_ref.get("connectionid") + statuscode = dv_ref.get("statuscode") if not connection_id: return [CheckResult(roles=_MAKER_ROLES, @@ -467,13 +485,31 @@ def _check_dv_connection(runner) -> list[CheckResult]: priority=Priority.HIGH.value, status=Status.FAILED.value, description=_DV_CONN_DESC, result=( - "The ESS Workday SOAP connection reference " - f"({wd_ref_name}) is unbound (connectionId=null)." + "The ESS Dataverse connection reference " + f"({dv_ref_name}) is unbound " + "(connectionid=null)." + ), + remediation=( + "In Power Platform / Copilot Studio, bind the Dataverse " + "connection reference to an active Dataverse connection owned " + "by your own account." + ), + doc_link=_DOC_SIMPLIFIED, + )] + + if statuscode != 1: + return [CheckResult(roles=_MAKER_ROLES, + checkpoint_id="DV-CONN-001", category=_CATEGORY, + priority=Priority.HIGH.value, status=Status.FAILED.value, + description=_DV_CONN_DESC, + result=( + "The ESS Dataverse connection reference " + f"({dv_ref_name}) is bound but inactive " + f"(statuscode={statuscode})." ), remediation=( - "In Power Platform / Copilot Studio, bind the Workday SOAP " - "connection reference to an active Workday connection owned by " - "your own account." + "Re-authenticate or re-bind the Dataverse connection so its " + "status is active, using an account you own." ), doc_link=_DOC_SIMPLIFIED, )] @@ -493,8 +529,9 @@ def _check_dv_connection(runner) -> list[CheckResult]: priority=Priority.HIGH.value, status=Status.PASSED.value, description=_DV_CONN_DESC, result=( - "The ESS Workday SOAP connection reference " - f"({wd_ref_name}) is bound to a connection." + owner_note + "The ESS Dataverse connection reference " + f"({dv_ref_name}) is bound to an active " + "connection." + owner_note ), doc_link=_DOC_SIMPLIFIED, )] diff --git a/solutions/ess-maker-skills/scripts/flightcheck/registry.py b/solutions/ess-maker-skills/scripts/flightcheck/registry.py index 9f348bb7f..dc759a9a1 100644 --- a/solutions/ess-maker-skills/scripts/flightcheck/registry.py +++ b/solutions/ess-maker-skills/scripts/flightcheck/registry.py @@ -540,16 +540,15 @@ class ResolvedPlan: priority=Priority.HIGH.value, roles=(Role.ESS_MAKER.value,), ), - # DV-CONN-001 — reads the Workday SOAP connection reference from the - # Declarative Agent minimalBots components API (AGENTBUILDER), plus a - # best-effort BAP owner echo (PP_ADMIN). + # DV-CONN-001 — self-contained Dataverse read (its own connectionreferences + # query) plus a best-effort BAP owner echo. CheckpointSpec( key="DV-CONN-001", category_fn=run_workday_extension_checks, category_label="Workday Extension", - clients=frozenset({AGENTBUILDER, PP_ADMIN}), + clients=frozenset({DATAVERSE, PP_ADMIN}), requires_config=True, - requires_dataverse_endpoint=False, + requires_dataverse_endpoint=True, priority=Priority.HIGH.value, roles=(Role.ESS_MAKER.value,), ), diff --git a/tests/flightcheck/checks/test_da_connection_refs.py b/tests/flightcheck/checks/test_da_connection_refs.py index e3b4c42e7..54f9034a7 100644 --- a/tests/flightcheck/checks/test_da_connection_refs.py +++ b/tests/flightcheck/checks/test_da_connection_refs.py @@ -106,6 +106,41 @@ def test_read_active_malformed_change_set_raises(): reader.read_active_agent_connection_references(runner) +def test_read_active_non_dict_change_entry_raises(): + # An individual change that is not an object is surfaced, not skipped. + runner = _FakeRunner( + _FakeClient({"BOT": {"connectionReferenceChanges": ["oops"]}}), + {"agent": {"botId": "BOT"}}, + ) + with pytest.raises(ValueError): + reader.read_active_agent_connection_references(runner) + + +def test_read_active_malformed_individual_reference_raises(): + # connectionReference present but not an object -> surfaced, not "not found". + payload = { + "connectionReferenceChanges": [ + {"changeType": "Insert", "connectionReference": "not-an-object"} + ] + } + runner = _FakeRunner(_FakeClient({"BOT": payload}), {"agent": {"botId": "BOT"}}) + with pytest.raises(ValueError): + reader.read_active_agent_connection_references(runner) + + +def test_read_active_change_without_reference_is_tolerated(): + # A change carrying no (or null) connectionReference is a non-connection + # change: skipped, not raised. + payload = { + "connectionReferenceChanges": [ + {"changeType": "Delete"}, + {"changeType": "Insert", "connectionReference": None}, + ] + } + runner = _FakeRunner(_FakeClient({"BOT": payload}), {"agent": {"botId": "BOT"}}) + assert reader.read_active_agent_connection_references(runner) == [] + + # -------------------------------------------------------------------------- # read_all_agents_connection_references (ENV-004 surface: env-wide, de-duped) # -------------------------------------------------------------------------- diff --git a/tests/flightcheck/checks/test_workday_extension.py b/tests/flightcheck/checks/test_workday_extension.py index 8983cc9ec..dbb97f857 100644 --- a/tests/flightcheck/checks/test_workday_extension.py +++ b/tests/flightcheck/checks/test_workday_extension.py @@ -10,9 +10,9 @@ connection, degrades gracefully when it does not. Cached-ref read + a best-effort Power Platform admin owner echo — no cassette required (the admin connections listing is the ``validated`` pp_admin mock). - * DV-CONN-001 — PASS/FAIL/SKIPPED over the validated minimalBots components - read (Workday SOAP connection reference; faked ``runner.agentbuilder``); - owner echo via the ``validated`` pp_admin mock. + * DV-CONN-001 — PASS/FAIL/NOT_CONFIGURED/SKIPPED over a documented-tier + Dataverse ``connectionreferences`` read (stubbed with ``responses``); owner + echo via the ``validated`` pp_admin mock. * WD-REST-001 — pure-config check (restBaseUrl trimmed to '/api'). * WD-REST-002 — pure local-file check (user-context redirect topic); SKIPPED on the legacy install path. @@ -28,18 +28,22 @@ from dataclasses import dataclass, field from typing import Any +import responses + from tests.conftest import require_validated_mock -from tests.mocks import agentbuilder_connectivity as ab from tests.mocks import dataverse as dv from tests.mocks import pp_admin as pp -require_validated_mock(ab) require_validated_mock(dv) require_validated_mock(pp) from flightcheck.checks import workday_extension as wx # noqa: E402 from flightcheck.runner import Priority, Role, Status # noqa: E402 +_DV_CONNECTOR_ID = ( + "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps" +) + # ───────────────────────────────────────────────────────────────────── # Minimal runner. The emitters read only these attributes; anything the @@ -58,17 +62,6 @@ def get_connections(self, _env_id: str): return self._connections -class _FakeAgentBuilder: - """Stand-in for FlightCheckRunner.agentbuilder. Only ``fetch_components`` - is consumed (DV-CONN-001's connection-reference read).""" - - def __init__(self, components: dict[str, Any]): - self._components = components - - def fetch_components(self, _agent_id: str): - return self._components - - @dataclass class _Runner: config: Any = field(default_factory=dict) @@ -76,7 +69,6 @@ class _Runner: env_url: str | None = None dv_token: str | None = None pp_admin: Any = None - agentbuilder: Any = None env_id: str | None = None agent_slug: str = "" _workday_connection_refs: list[dict[str, Any]] = field(default_factory=list) @@ -98,6 +90,28 @@ def _by_id(results): return {r.checkpoint_id: r for r in results} +def _dv_ref(*, connection_id, statuscode=1): + """A Dataverse connection reference matching the extension pack's shipped + ref (connector shared_commondataserviceforapps, logical-name suffix + 92b66).""" + return dv.connection_ref( + logical_name="msdyn_sharedcommondataserviceforapps_92b66", + display_name="Microsoft Dataverse", + connector_id=_DV_CONNECTOR_ID, + connection_id=connection_id, + statuscode=statuscode, + ) + + +def _register_refs(base_url: str, refs: list[dict[str, Any]]) -> None: + responses.add( + method="GET", + url=f"{base_url}/api/data/v9.2/connectionreferences", + json=dv.collection(refs), + status=200, + ) + + # ───────────────────────────────────────────────────────────────────── # WD-CONN-AUTH-001 — always MANUAL echo (S5.3). # ───────────────────────────────────────────────────────────────────── @@ -241,111 +255,147 @@ def test_never_passes_regardless_of_state(self): # ───────────────────────────────────────────────────────────────────── -# DV-CONN-001 — Workday SOAP connection binding (S5.4, PASS/FAIL). +# DV-CONN-001 — Dataverse connection binding (S5.4, PASS/FAIL). # ───────────────────────────────────────────────────────────────────── -def _runner_with_refs(references, *, pp_admin=None, env_id=None): - """A runner whose faked ``agentbuilder.fetch_components`` returns the given - connection references and whose config names an active agent (botId).""" - components = ab.components_with_references(references=references) - return _Runner( - config={"agent": {"botId": ab.MOCK_AGENT_ID}}, - agentbuilder=_FakeAgentBuilder(components), - pp_admin=pp_admin, - env_id=env_id, - ) - - class TestDataverseConnection: - def test_bound_with_owner_echo_passes(self): + @responses.activate + def test_bound_active_with_owner_echo_passes( + self, fake_dataverse_url, fake_token + ): + _register_refs( + fake_dataverse_url, + [_dv_ref(connection_id="dv-conn-active", statuscode=1)], + ) owner_conn = pp.connection( - name="wd-conn-active", - api_name="shared_workdaysoap", + name="dv-conn-active", + api_name="shared_commondataserviceforapps", extra_properties={"accountName": "maker@contoso.com"}, ) - runner = _runner_with_refs( - [ab.workday_connection_reference(connection_id="wd-conn-active")], + runner = _Runner( + env_url=fake_dataverse_url, + dv_token=fake_token, pp_admin=_FakePPAdmin([owner_conn]), env_id="env-1", ) r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] assert r.status == Status.PASSED.value - assert "bound to a connection" in r.result + assert "bound to an active" in r.result assert "maker@contoso.com" in r.result assert "your own account" in r.result - def test_passes_without_pp_admin_notes_owner_unreadable(self): - runner = _runner_with_refs( - [ab.workday_connection_reference(connection_id="wd-conn-active")], + @responses.activate + def test_passes_without_pp_admin_notes_owner_unreadable( + self, fake_dataverse_url, fake_token + ): + _register_refs( + fake_dataverse_url, + [_dv_ref(connection_id="dv-conn-active", statuscode=1)], ) + runner = _Runner(env_url=fake_dataverse_url, dv_token=fake_token) r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] assert r.status == Status.PASSED.value assert "owner could not be read" in r.result assert "your own account" in r.result - def test_unbound_fails(self): - runner = _runner_with_refs( - [ab.workday_connection_reference(connection_id=None)], + @responses.activate + def test_runtime_dataverse_reference_passes( + self, fake_dataverse_url, fake_token + ): + runtime_ref = dv.workday_connection_refs_runtime()[1] + _register_refs(fake_dataverse_url, [runtime_ref]) + runner = _Runner( + env_url=fake_dataverse_url, + dv_token=fake_token, ) + + r = _by_id( + wx.run_workday_extension_checks(runner) + )["DV-CONN-001"] + + assert r.status == Status.PASSED.value + assert ( + "msdyn_sharedcommondataserviceforapps_workdayruntime" + in r.result + ) + + @responses.activate + def test_mixed_runtime_and_legacy_dataverse_refs_warn( + self, fake_dataverse_url, fake_token + ): + runtime_ref = dv.workday_connection_refs_runtime()[1] + _register_refs( + fake_dataverse_url, + [_dv_ref(connection_id="legacy-dv"), runtime_ref], + ) + runner = _Runner( + env_url=fake_dataverse_url, + dv_token=fake_token, + ) + r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] - assert r.status == Status.FAILED.value - assert "unbound" in r.result - assert "connectionId=null" in r.result - assert "bind the Workday SOAP connection reference" in r.remediation + assert r.status == Status.WARNING.value + assert "Multiple ESS Dataverse connection references" in r.result + assert "Remove obsolete Workday package references" in r.remediation - def test_workday_ref_absent_fails(self): - # Only a ServiceNow ref is present - no Workday SOAP ref. - runner = _runner_with_refs( - [ - ab.connection_reference_change( - connector="shared_service-now", - connection_id="sn-1", - ) - ] + @responses.activate + def test_unbound_fails(self, fake_dataverse_url, fake_token): + _register_refs( + fake_dataverse_url, [_dv_ref(connection_id=None, statuscode=1)] ) + runner = _Runner(env_url=fake_dataverse_url, dv_token=fake_token) r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] assert r.status == Status.FAILED.value - assert "was not found" in r.result - assert "shared_workdaysoap" in r.result - assert "Install or repair the Workday extension pack" in r.remediation + assert "unbound" in r.result + assert "connectionid=null" in r.result + assert "bind the Dataverse connection reference" in r.remediation - def test_no_agentbuilder_client_skips(self): - runner = _Runner(config={"agent": {"botId": ab.MOCK_AGENT_ID}}) + @responses.activate + def test_inactive_statuscode_fails(self, fake_dataverse_url, fake_token): + _register_refs( + fake_dataverse_url, + [_dv_ref(connection_id="dv-conn-inactive", statuscode=2)], + ) + runner = _Runner(env_url=fake_dataverse_url, dv_token=fake_token) r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] - assert r.status == Status.SKIPPED.value - assert "not available" in r.result - - def test_no_active_agent_botid_skips(self): - runner = _Runner( - config={}, - agentbuilder=_FakeAgentBuilder(ab.components_with_references()), + assert r.status == Status.FAILED.value + assert "inactive" in r.result + assert "statuscode=2" in r.result + assert "Re-authenticate or re-bind" in r.remediation + + @responses.activate + def test_missing_ref_not_configured(self, fake_dataverse_url, fake_token): + # Only a Workday ref present — no Dataverse (92b66) ref. + _register_refs( + fake_dataverse_url, + [ + dv.connection_ref( + logical_name="new_sharedworkdaysoap_ff0df", + display_name="OAuthUser", + connector_id=dv.WORKDAY_SOAP_CONNECTOR_ID, + connection_id="wd-conn-1", + ) + ], ) + runner = _Runner(env_url=fake_dataverse_url, dv_token=fake_token) r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] - assert r.status == Status.SKIPPED.value - assert "not available" in r.result + assert r.status == Status.NOT_CONFIGURED.value + assert "was not found in this environment" in r.result + assert "Install/repair the Workday extension pack" in r.remediation - def test_malformed_changeset_degrades_to_warning(self): - # A 200 payload whose connectionReferenceChanges is present but not a - # list is a shape we do not understand: fail loudly (dispatcher WARNING) - # rather than reporting a confident "reference not found" FAILED. - runner = _Runner( - config={"agent": {"botId": ab.MOCK_AGENT_ID}}, - agentbuilder=_FakeAgentBuilder( - {"connectionReferenceChanges": {"unexpected": "dict"}} - ), - ) + def test_no_dv_token_skips(self): + runner = _Runner(env_url="https://x.crm.dynamics.com", dv_token="") r = _by_id(wx.run_workday_extension_checks(runner))["DV-CONN-001"] - assert r.status == Status.WARNING.value - assert "Unable to run DV-CONN-001" in r.result - assert "DV-CONN-001" in r.remediation + assert r.status == Status.SKIPPED.value + assert "Dataverse token not available" in r.result # ───────────────────────────────────────────────────────────────────── diff --git a/tests/flightcheck/test_registry.py b/tests/flightcheck/test_registry.py index ca696abe2..e8882af57 100644 --- a/tests/flightcheck/test_registry.py +++ b/tests/flightcheck/test_registry.py @@ -294,7 +294,7 @@ class TestWorkdayExtensionCheckpoints: """skill-5 mints five checkpoints, all sharing checks/workday_extension.run_workday_extension_checks, category "Workday Extension". Two are always-MANUAL echoes/attestations, three are - programmatic (one minimalBots components read + two pure-local).""" + programmatic (one Dataverse read + two pure-local).""" _ALL = ( "WD-CONN-AUTH-001", @@ -329,12 +329,10 @@ def test_conn_auth_exact_beats_wd_conn_family(self): assert registry.resolve("WD-CONN-AUTH-001").key == "WD-CONN-AUTH-001" assert registry.resolve("WD-CONN-AUTH-001").is_family is False - def test_dv_conn_spec_declares_agentbuilder_and_pp_admin(self): + def test_dv_conn_spec_declares_dataverse_and_pp_admin(self): spec = registry.resolve("DV-CONN-001") - assert spec.clients == frozenset( - {registry.AGENTBUILDER, registry.PP_ADMIN} - ) - assert spec.requires_dataverse_endpoint is False + assert spec.clients == frozenset({registry.DATAVERSE, registry.PP_ADMIN}) + assert spec.requires_dataverse_endpoint is True assert spec.prereqs == () assert Role.ESS_MAKER.value in spec.roles @@ -356,7 +354,7 @@ def test_net_check_is_clientless_and_ppadmin_gated(self): def test_dv_conn_plan_unions_clients(self): plan = registry.transitive_requirements("DV-CONN-001") - assert registry.AGENTBUILDER in plan.clients + assert registry.DATAVERSE in plan.clients assert registry.PP_ADMIN in plan.clients def test_all_five_are_listable(self):