QA Use Cases — MCP server (Namespace as a Service)
Tool page: tools/namespace-service.md;
backend: tools/servicenow-pdi.md.
The rule every story hangs on: the agent is the front door,
never the authority — all validation is deterministic and
server-side, re-run at plan and at creation (source:
namespace-service.md). Endpoint: https://mcp.rhels.com/mcp.
Since 2026-10-29 the server runs on pro (the front is
unchanged); both instances (mini-fronted pro + CRC, when running)
run the same code from rhels/lab-agentops with SNOW_BACKEND
selecting the real PDI or the simulator.
Admin stories
MCP-A1 — Tool list intact [config/API]
- Persona: Platform admin.
- Steps: connect, list tools.
- Expected: the tool list contains the nine documented
tools —
get_bsn_details,verify_requester,list_eligible_clusters,check_namespace_name,ensure_ldap_group,plan_namespace,create_namespace_request,get_request_status,list_my_namespaces— plus theloginsession tool the flow begins with (source: namespace-service.md, “The nine tools” + flow step 2). - Assert: MCP
tools/listname set == the manifest set stored as data; the manifest is pinned from the live server at suite build (see Gaps — the book’s “nine” and the login tool make the raw count ambiguous in prose, which is exactly why the set lives as data).
MCP-A2 — Backend mode is the recorded mode [config/API]
- Persona: Platform admin.
- Steps: read the server’s effective
SNOW_BACKEND; callget_bsn_detailsfor a known BSN (app1001) and confirm the record’s provenance. - Expected: the platform instances run against the real
PDI (
pdi) — both have since 2026-10-15; sim is for exercises only (source: namespace-service.md intro; servicenow-pdi.md). - Assert:
get_bsn_details(app1001)→ record matches the PDI registry row (app name “Payments Demo”, segment, groups) — a sim-shaped answer on a platform instance is drift.
MCP-A3 — Session issuance and identity binding [config/API]
- Persona: Platform admin probing with fixture accounts.
- Steps:
loginas dev1; call a discovery tool with the token; call again with a tampered/absent token; let a token age past its TTL (or inspect the TTL constant). - Expected: token issued on a real LDAP bind, opaque, 8-hour TTL; identity derives from the token alone; no token → refused; wrong password → 401 (source: namespace-service.md, step 2 of the flow).
- Assert: login(bad password) →
401; tool call without token → refusal; tool call with token → caller’s identity ==dev1in the server-side audit counters (per-tool call and validation-failure metrics exist and increment — source: “Audit is the PR” + Prometheus counters).
MCP-A4 — Server holds no cluster-admin [config/API]
- Persona: Platform admin. Goal: the security model’s load-bearing claim.
- Steps: read the MCP server’s ClusterRole; confirm its only write path is git via the GitHub API.
- Expected: a read-only ClusterRole (status aggregation); no cluster write verbs; the GitHub token arrives as an out-of-band Secret, never committed (source: namespace-service.md, “Security model”).
- Assert: rendered ClusterRole rules contain no
create,update,patch, ordeleteverbs on workload resources; repo grep for token-shaped strings inservices/mcp-server/→ zero (the CI gitleaks gate is the standing control).
User stories
MCP-U1 — Happy path: BSN to namespace via PR [config/API]
- Persona:
dev1through an app-team agent. - Preconditions: a fresh suffix on app1001’s non-prod cluster.
- Steps:
login→get_bsn_details(app1001)→verify_requester→list_eligible_clusters→check_namespace_name→plan_namespace(defaults 500m / 512Mi) → confirm →create_namespace_request→ merge the PR →get_request_status. - Expected: the PR adds exactly one
requests/app1001-<suffix>.yaml; after merge + sync, status reports the namespace live, the chart-rendered ResourceQuota and theapp1001-nonprodRoleBinding (admin in non-prod) in place; a dummy pod runs (source: namespace-service.md — the worked request; the six-hour E2E cycle runs this story unattended under the standing authorization on lab-monorepo #30). - Assert: PR file set ==
{requests/app1001-<suffix>.yaml}; post-merge: namespace exists with BSN labels; RoleBinding roleRef ==admin(nonprod) for groupapp1001-nonprod;get_request_statusstage 1 == complete. (Stages 2–3 are MCP-U3’s clocks, not this story’s gate.)
MCP-U2 — Non-member is refused, and nothing is written (negative) [config/API]
- Persona:
dev2(valid directory user, not inapp1001-devsupport). - Steps:
login→verify_requester(app1001)→plan_namespace(app1001, …)→ attemptcreate_namespace_request. - Expected: verify fails in plain language naming the group to join; plan fails; create refuses — and no commit or PR results. “Proven negatively” is the documented standard (source: namespace-service.md, “Security model”).
- Assert: verify/plan responses carry a failure naming
app1001-devsupport;git logonrhels/lab-agentopsshows no new commit and no new PR from the attempt (compare HEAD + open-PR count before/after).
MCP-U3 — Status tells the three clocks apart [config/API]
- Persona:
dev1, checking the MCP-U1 request later. - Steps:
get_request_statusat intervals after creation. - Expected: three completions reported separately — namespace live; team can log in (group exists and a group-sync cycle carried it); workloads can read secrets (VaultAuth present; the per-BSN Vault policy lands on its own clock). A pending group shows as pending with its ticket number, not as an error (source: namespace-service.md, “three completions, three clocks”).
- Assert: response contains three distinct stage states; no stage borrows another’s value (e.g. stage 2 != complete while the group is absent from the directory).
MCP-U4 — Injection cannot reach the request file (negative) [config/API]
- Persona:
dev1, adversarially (theapp1001-securefixture pattern). - Steps: plan/create with hostile quota strings (YAML
payloads in
cpu/memory). - Expected: strict quantity validation refuses; nothing is committed. This is the regression story for the POC’s one critical finding (source: namespace-service.md, “Injection hardening is regression-tested”).
- Assert: create → validation failure; HEAD unchanged; the committed-file grep for the payload → zero.
Gaps
- Known behavior change (2026-10-29): since the server
moved to pro,
get_request_status’s Argo/namespace stages read pro’s API and degrade to not-found for mini-fulfilled namespaces (git/LDAP/ServiceNow stages stay exact). MCP-U3’s Assert must be read with that scope until the design call lands — recorded on lab-monorepo#42. - TBD: no documented expectation for plan freshness (how long a confirmed plan stays creatable before re-planning is required).
- Manual-only: none for the coded stories; the conversational variant of MCP-U1 (a human chatting with an agent) stays a periodic manual exercise by design — the book’s own test standard is “a scripted MCP client run and a conversational agent run” (namespace-service.md).