[4/5] Expose the scoped announcement MCP runtime - #272
rebova-microsoft wants to merge 1 commit into
Conversation
Expose separate announcement manager and editor entry points with validated flat arguments, scoped retry context, captured directory leases, and widget-owned mutations. Preserve indeterminate and committed-refresh recovery, safe telemetry, and independent provider discovery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eddd3818-bb74-42d3-bcf3-7e0670a57f27
| tests/mcp/agentconfig_org_announcements/test_authoring_client.py | ||
| tests/mcp/agentconfig_org_announcements/test_drafts.py | ||
| tests/mcp/agentconfig_org_announcements/test_graph_directory.py | ||
| tests/mcp/agentconfig_org_announcements |
There was a problem hiding this comment.
Walkthrough — Include the runtime checks in CI.
The announcements job now selects the whole feature test directory instead of naming three individual files. That includes the new protocol, client-lifecycle, and telemetry tests alongside the earlier client, draft, and directory tests, without adding a workflow entry for each file. The foundation job also gains test_import_isolation.py, which checks that the sibling MCP providers load their own modules. These changes expand test selection; they do not start or deploy the announcements service.
| meta=_widget_tool_meta(), | ||
| annotations=_READ_ONLY_ANNOTATIONS, | ||
| ) | ||
| async def open_org_announcements( |
There was a problem hiding this comment.
Walkthrough — Connect requests to announcement services.
This file connects requests to PR #270's authoring client and PR #271's Graph client—not screen layout or storage.
org_announcements_widgetreturns a static HTML shell loading Vorpal. Metadata exposes read tools to the model/app and mutations only to the app.open_org_announcementstakes flattitleId/viewarguments. Manager has nomode; editor requirescreateoredit, with an ID only for edit. Pydantic checks combinations before widget-error handling: malformed calls becomeToolError; valid failed opens retain the original request without inventing an ID/mode. Opening never saves.- Each operation captures its authoring client and authenticated tenant alongside
titleId; directory work also binds the account.get_graph_clientcounts active users so replacing a tenant/account client does not close it mid-operation. save_bulletinsends complete content/audience.transition_bulletindelegates identifier/status-only changes; publishing uses a full save, not a republish mode.duplicate_bulletinstrips identity/audit fields and creates a Draft before any later editor opening, preserving its title and leaving the source unchanged._saved_item_resultcombines acknowledged content with refreshed manager/audience data.IndeterminateWriteandCommittedRefreshFailedare non-retryable results: inspect current state instead of blindly repeating a create/copy. A manager refresh does not guarantee identifying the saved record.
| return _ADK_TELEMETRY | ||
|
|
||
|
|
||
| def record_operation( |
There was a problem hiding this comment.
Walkthrough — Measure operations without recording authored content.
record_operation adapts the runtime's calls to the existing adk.api.call emitter; it is not a separate widget-event bridge. The api_endpoint field holds an allowlisted tool name, not the service URL. Success adds outcome and duration; failure also adds a short identifier-shaped error code and a broad backend/Graph/MCP category, with an empty error message. Unknown operation/source values are replaced, and message-shaped or overlong codes become UnknownError. Error codes are shape-checked, not a fixed allowlist. The helper resolves the emitter lazily and tolerates its absence or emission exceptions, keeping those telemetry failures separate from the tool result.
|
|
||
|
|
||
| @pytest.mark.parametrize("modules", [LANDING]) | ||
| @pytest.mark.parametrize("modules", [LANDING, ANNOUNCEMENTS]) |
There was a problem hiding this comment.
Walkthrough — Share discovery without coupling the feature providers.
Adding announcements to the existing parameterized tests applies the same discovery contract to both clients: shared helper methods, preserved titleId values, expected request routes, rejected invalid searches, and errors for malformed collections rather than empty successes. The feature-tool check uses a discovery-only fake, so listing/searching agents does not require configuration initialization or attach a widget. Announcements awaits its client and advertises readOnlyHint; the landing-page provider keeps its existing synchronous accessor and unchanged metadata. Search uses a POST request here, but its purpose is still to find existing agents, not create configuration.
| assert org_server._client is None | ||
|
|
||
|
|
||
| def test_the_authoring_client_is_constructed_off_the_event_loop( |
There was a problem hiding this comment.
Walkthrough — Keep sign-in from blocking the request transport.
These tests cover how the server obtains and replaces its authoring client, rather than announcement content. Fakes check that construction happens off the async event-loop thread, concurrent first requests share one construction, and reset drops and closes the old client so a subsequent call can build another. Authoring-side 401 failures trigger that reset; Graph authentication failures and other tested service statuses do not. Mocked sign-in paths also check that notices go to stderr, not stdout, because stdout carries MCP protocol messages. Additional guards look for stdout prints and overly verbose HTTP logging. No real browser sign-in is needed for these scenarios.
| return install | ||
|
|
||
|
|
||
| def _call(tool: str, arguments: dict[str, Any], *, include_scope: bool = True) -> Any: |
There was a problem hiding this comment.
Walkthrough — Exercise the contract the host and widget call.
_call goes through FastMCP's tool dispatcher, while fake authoring/directory clients record requests and inject failures. This covers resource metadata, tool visibility, structured results, complete saves, minimal transitions, duplicate creation, and audience lookup. The schema assertions intentionally require only titleId and view globally; separate invocation tests check the conditional manager/create/edit rules and ToolError boundary. Valid failed opens retain retry context instead. Other cases distinguish uncertain writes from acknowledged writes whose refresh fails, checking that the mutation is not repeated. Scope and overlapping-request cases check captured tenant/agent/account context and Graph-client replacement. These are host/runtime contract checks with simulated dependencies, not evidence about rendered UI or deployed backend behavior.
|
|
||
|
|
||
| @pytest.fixture | ||
| def emitted(monkeypatch) -> list[dict[str, Any]]: |
There was a problem hiding this comment.
Walkthrough — Inspect what reaches the telemetry emitter.
The emitted fixture replaces the ADK emitter and records its fields, so assertions inspect the outgoing telemetry payload rather than assuming a caller omitted sensitive data. Tests cover exact success/failure field shapes, normalization of unexpected values, and missing or failing emitters. Mocked tool calls carry announcement text, audience IDs, and tenant/agent/account context; assertions check that the fixture values, suggested drafts, search text, and echoed backend messages stay out of the tested events. A final guard limits field names across the five instrumented operations. This targets the telemetry boundary, not a blanket ban on content in legitimate tool responses.
| ) | ||
|
|
||
|
|
||
| def test_both_servers_load_with_their_own_client_module() -> None: |
There was a problem hiding this comment.
Walkthrough — Prevent sibling servers from importing each other's code.
Both MCP providers have files named client.py and server.py. Loading them under those plain module names in one Python process can make the second provider reuse the first provider's module. These tests check the shared isolated loader returns distinct clients from the correct directories, reuses each provider's own modules on repeated loads, and restores previous plain-name entries. A source scan discourages direct flat imports in test modules; a separate subprocess checks that both providers coexist in one interpreter. This protects the test harness from misleading cross-provider failures, not tenant isolation or live service routing.
Description
Expose the separate announcements MCP provider, widget resource, manager/editor opener, audience lookup, and widget-owned authoring operations.
Current diff
Feature contract
The opener and discovery tools are read-only. The widget owns mutations after opening. Unknown ownership and malformed retry requests are not accepted. Backend publishing capability and existing Duplicate/Unarchive write timing are unchanged.
Stack and dependency
This is slice 4/5. Head:
users/rebova/org-announcements-review-runtime. Base:users/rebova/org-announcements-review-audience. The current diff is only this slice against its immediate predecessor; these are stacked review chunks, not parallel PRs against the integration branch.Bootstrap prerequisite: #262 merged into
release/planner-landing-pageon September 10, 2026. This stack remains pinned tocacb1bec056428809f1ccb0383561190d516bee4, which is an ancestor of merge commit8a04f5f40f334e729a3497877edca655730f1be2. Unchanged prerequisite work is excluded from this slice. No rewrite is needed solely to account for that merge. The future release target remains TBD and its final promotion baseline must be confirmed separately.Testing
612 Python tests passed from the clean committed slice, including protocol/recovery, privacy, cross-provider discovery, import isolation, and existing landing-page tests. Ruff and syntax checks passed.
Local execution used Python 3.13.15. Python 3.11 GitHub Actions validation is pending; no local 3.11 pass is claimed.
Readiness