test(seller-agent): add MCP-level tool tests and cover force_account_status - #485
Conversation
The context-agent HTTP handler (targeting/contextagent/handler.go)
echoed tmproto.ValidateContextRequest's raw err.Error() text straight
into the HTTP error response body, e.g. "property_id contains invalid
characters" or "seller_agent_url exceeds maximum length of 2048". This
violates AGENTS.md's generic-error-message invariant ("Never echo
err.Error() in HTTP responses ... details go to structured logs
(slog) inside the service boundary") and, unlike every other place
that runs the same ValidateContextRequest/ValidateIdentityRequest
check, never logged the failure server-side at all — so an operator
had zero record of why a request was rejected.
This exact pattern was swept and fixed elsewhere in this repo by
#190/#201 (landed in PR #210): router.HandleContextMatch,
router.HandleIdentityMatch, and identityagent's ServeHTTP were all
changed to log the validation error server-side via a
logValidationFailure helper and return a generic "invalid request"
message to the caller. That sweep did not touch
targeting/contextagent/handler.go, which is the handler actually
wired into the cmd/context-agent binary, so it kept the pre-#210
behavior.
Fix: add a logValidationFailure helper to the context-agent handler,
mirroring identityagent's implementation exactly (method, path, error,
and either request_id or request_id_valid=false, logged via
slog.Warn), and replace the err.Error()-in-response call with the
generic "invalid request" message.
Verification: confirmed via git blame that this line predates and was
untouched by the #210 sweep, and confirmed no open issue or PR already
covers this file. Regression tests added in handler_test.go assert
the HTTP response never contains the validator's field-specific text
while the structured log does; both tests were checked to fail against
the pre-fix source (reverting the handler.go change while keeping the
tests) and pass after restoring it.
…status reference/seller-agent already had strong backend-method-level tests for most of issue #140's acceptance list (create_media_buy, pending_creatives -> active, cancel/double-cancel, list_creatives filtering, delivery reporting, and the seed_product/seed_pricing_option/force_create_media_buy_arm custom comply_test_controller scenarios), added incrementally after #138 merged. Two real gaps remained: - forceAccountStatus (the force_account_status compliance scenario) had no test at all. - every existing test calls backend methods directly, bypassing the actual adcp.Register/adcp.AddTool/adcp.RegisterTestController wiring main() uses to serve requests — so a regression in tool names or request/response field mapping would only be caught by the npm storyboard runner. Extract newServer(b *backend) *mcp.Server out of main() (pure refactor, no behavior change) so tests can stand up the real registered server over an in-memory MCP transport, and add main_mcp_test.go covering every bullet in the issue through actual tool calls: create_media_buy with/without creative assignments, pending_creatives -> active via sync_creatives and update_media_buy, cancel + double-cancel (asserting the specific NOT_CANCELLABLE code), list_creatives filtering by creative_id and format_id, delivery simulation/reporting via comply_test_controller + get_media_buy_delivery, and the seed_product/force_create_media_buy_arm custom scenarios end to end. Verified the new tests catch real regressions, not just compile: breaking forceAccountStatus's status assignment, the double-cancel guard, and simulateDelivery's spend accumulation each failed the corresponding new test (the double-cancel and delivery breaks also exposed that the existing tests check IsError/spend but not the specific error code the new MCP test asserts), then restored and reconfirmed all tests pass. Closes #140.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — no blocking or medium findings.
Checked:
targeting/contextagent/handler.go— validator detail now routed to structuredslog.Warn; response returns generic "invalid request" instead of echoingerr.Error(), closing an info-leak/observability gap. Response shape and HTTP status unchanged, so no breaking-wire-change marker needed; commit usesfix(targeting).reference/seller-agent/cmd/seller-agent/main.go—newServerextraction is a behavior-preserving refactor, covered by new MCP-level andforce_account_statustests.- No schema/
types_gen.go,tmprotosigning/verify, identity-agent TEE, or protocol-managed skills surfaces touched.
Decision-table application: no critical/high/medium findings. gated_paths is false (row 2 N/A). high_risk is true only via (modified)/(added) entries with no medium finding (rows 3–5 N/A). No prior decision, no no-auto-approve team match. Falls through to row 9 → approve. (review_decision: REVIEW_REQUIRED does not block since gated_paths is false and no team gate applies.)
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean delta pass, no blocking or medium findings.
Checked:
- contextagent validation path now returns a generic "invalid request" response instead of echoing
tmproto.ValidateContextRequest'serr.Error(), closing an info-leak of field-shaped detail. Real error is logged server-side via a newlogValidationFailurehelper. - The helper verbatim-mirrors the identityagent and router implementations (same
SafeRequestIDForEchoguard andrequest_id_valid=falseelision), so behavior is consistent across agents. - Two new tests pin the generic-response and unsafe-request_id-not-echoed invariants.
High-risk flag is true only because the change touches targeting/** (one modified handler, one added test file) — the reviewer surfaced no medium-or-higher concerns on the modified file, so the modification is presumed safe. No deletions. gated_paths is false. No no-auto-approve team match. This is a net improvement over the prior approve with no new findings. Falls through to row 9.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — test-only refactor with no findings.
This PR touches three files, all in reference/seller-agent/cmd/seller-agent/: main.go, main_mcp_test.go, and main_test.go. The change extracts main()'s server-wiring closure verbatim into a newServer(b) helper (behavior-preserving: same Register/AddTool calls, same ADCP_SANDBOX guard, same shared backend per connection) and adds MCP in-memory-transport integration tests plus a TestForceAccountStatus unit test.
No sensitive surfaces touched: no wire shapes, no adcp/schemas/** or adcp/types_gen.go, no tmproto signing/verification, no identity-agent TEE boundary, no protocol-managed skills. Schema↔types coherence and TMP semantics are unaffected.
Decision-table walk: no critical/high findings (row 1 n/a); gated_paths is false and review_decision is APPROVED anyway (row 2 n/a); high_risk is false (rows 3/5 n/a); no medium findings (rows 4/8 n/a); no no-auto-approve team match (row 7 n/a); prior decision was approve (row 6 n/a). Falls through to row 9 → approve.
The reviewer returned a clean pass with zero findings.
Policy-compliant relay of external contribution #475 so repository-default CodeQL can run. The original contributor remains the commit author; maintainer and Ladon reviews plus CI passed on #475.
Summary
Closes #140.
reference/seller-agentalready had strong, honest state-machine coverage for most of the issue's acceptance bullets — added incrementally after #138 merged (see #195, #245, and follow-on work). Before writing anything I readcmd/seller-agent/main_test.goin full to avoid duplicating that work, and cross-checked it line-by-line against every bullet in the issue:TestCreateMediaBuy_WithoutCreatives,TestCreateMediaBuy_WithCreatives)pending_creatives->activeviasync_creativesandupdate_media_buy— already coveredTestCancellation,TestDoubleCancellation)list_creativesfiltering by creative ID and format ID — already coveredcomply_test_controllerscenarios (seed_product,seed_pricing_option,force_create_media_buy_arm) — already coveredTwo real gaps remained, which this PR fixes:
force_account_statushad zero test coverage. It's one of the seller's ownTestControllerStoremethods (b.forceAccountStatus), wired intocomply_test_controller, and nothing exercised it.b.createMediaBuy(...),b.updateMediaBuy(...), etc.), which exercises the state machine but bypasses the actualadcp.Register/adcp.AddTool/adcp.RegisterTestControllerwiringmain()uses to serve requests. A regression in tool names, or in how a request/response gets JSON-mapped onto the wire, would compile fine and pass every existing test — the npm storyboard runner would be the only thing to catch it, which is exactly what the issue says shouldn't be the only safety net.What's added
newServer(b *backend) *mcp.Serverout ofmain()so tests can stand up the exact servermain()runs, over an in-memory MCP transport. Verified withgit diff -bthat this is a pure whitespace/extraction diff — no logic changed.main_mcp_test.go(new): 10 tests that call the real registered tools by name with JSON arguments (the way an MCP client, including the storyboard runner, would), covering every bullet in the issue's acceptance list end-to-end:create_media_buywith/without creative assignments,pending_creatives -> activeviasync_creativesandupdate_media_buy, cancel + double-cancel (asserting the specificNOT_CANCELLABLEcode, not just "is an error"),list_creativesfiltering bycreative_id/format_id, delivery simulation+reporting viacomply_test_controller(simulate_delivery) +get_media_buy_delivery, and theseed_product/force_create_media_buy_armcustom scenarios through the wire.main_test.go: 2 new unit tests closing the one real gap —TestForceAccountStatusandTestForceAccountStatus_NotFound.Verification that these tests catch real regressions, not just compile
Per this project's own bar for proving a test is load-bearing, I broke three separate pieces of real logic, confirmed the new tests failed for the right reason, then restored the fix and reconfirmed everything passes:
acct.Status = statusinforceAccountStatus→TestForceAccountStatusfailed (forceAccountStatus did not persist new status, got active).updateMediaBuy→TestMCP_CancelAndDoubleCancelfailed (want NOT_CANCELLABLE on double-cancel, got "INVALID_TRANSITION") — and notably, the pre-existingTestDoubleCancellationkept passing, because it only assertsIsError, not the specific code. This is a concrete example of the MCP-level tests catching something the existing suite didn't.simulateDelivery'sweightedSpendcall → both the newTestMCP_DeliverySimulationAndReportingand the pre-existingTestDeliveryReporting_SimulateDelivery/TestDeliveryReporting_SimulateDeliveryWeightsSpendByBudgetcorrectly failed.Acceptance criteria
cd reference/seller-agent && go test ./...— 44 tests, all passing, exercising every state transition in the issue (create/no-create, activation via both paths, cancel/double-cancel, filtering, delivery, and all three custom scenarios) without the npm storyboard runner.scripts/ci/run_storyboard_reference_seller.shfrom [codex] Add Go reference seller storyboard harness #138) is untouched and remains the end-to-end compliance check.Validation
All green.
git statusis clean — this PR touches exactlyreference/seller-agent/cmd/seller-agent/{main.go,main_test.go,main_mcp_test.go}.Note on scope
I did not add a general integration test for
get_media_buysaccount-scoping,sync_accounts, or the standardforce_media_buy_status/force_creative_statusscenarios beyond what already existed, since those are outside the issue's explicit bullet list and already have direct or indirect coverage (TestForceMediaBuyStatus*,TestImpairmentsRequireMaterialCreativeImpactAndNewReopenIDexercisingforceCreativeStatus). Happy to extend if maintainers want broader coverage in the same pass.