Skip to content

Pagination integrity for get_media_buys #3111

@bokelley

Description

@bokelley

Goal

Fourth in the rolling pagination conformance series (#3095 cursor↔has_more lint + list_creatives storyboard, #3100 total_count honesty, #3109 get_signals — all merged or in flight). This issue extends the same gates onto `get_media_buys`.

Why `get_media_buys` is the next clean target

  • Has `pagination` in both request and response schemas.
  • `comply_test_controller` already exposes `seed_media_buy` (analogous to `seed_creative`), so the storyboard can pre-populate a known fixture set and pin counts deterministically — same shape as `pagination_integrity.yaml`.
  • `handleGetMediaBuys` at `server/src/training-agent/task-handlers.ts:1642` currently emits no `pagination` block. With small fixture sets the bug is dormant but a real seller with hundreds of media buys would silently truncate.

What's needed

1. Training agent fix (`server/src/training-agent/task-handlers.ts:1642`)

Mirror the `handleListCreatives` pattern:

  • Read `req.pagination?.max_results` (default 50, cap 100 per the schema).
  • Decode `req.pagination?.cursor` via the offset-cursor codec (`encodeOffsetCursor('media_buys', n)` / `decodeOffsetCursor('media_buys', cursor)` — added in feat(compliance): get_signals pagination cursor integrity #3109).
  • Slice `buys` by `[offset, offset + max_results]`.
  • Emit `pagination: { has_more, total_count, ...(hasMore && { cursor }) }`.
  • Return `INVALID_REQUEST` when the cursor is malformed.

2. Storyboard (`static/compliance/source/universal/get-media-buys-pagination-integrity.yaml`)

Mirror `pagination-integrity.yaml` (the seeded list_creatives one):

  • `controller_seeding: true` with three `media_buys` entries in `fixtures:`. Pick stable IDs and minimal valid shape (status, currency, dates).
  • Phase 1: `get_adcp_capabilities` (capability discovery boilerplate).
  • Phase 2: pagination walk
    • Step 1 (`first_page`): `get_media_buys` with `pagination.max_results: 2`. Capture `pagination.cursor` into `$context.next_cursor`. Assert `pagination.has_more = true`, `pagination.cursor` present, `pagination.total_count = 3` if volunteered (`field_value_or_absent allowed_values: [3]`), `query_summary` consistency where the schema declares it (check the response schema — get-media-buys-response may not have `query_summary`; if not, drop those assertions).
    • Step 2 (`terminal_page`): `get_media_buys` with `pagination.cursor: $context.next_cursor, max_results: 2`. Assert `pagination.has_more = false`, `pagination.cursor` absent or null, `pagination.total_count = 3` if volunteered.

3. Wiring

  • Add a changeset (empty, descriptive name like `get-media-buys-pagination-integrity.md`).
  • Confirm `build:compliance` picks up the new storyboard (count goes 14 → 15 universal).
  • Run `pagination_integrity` filter against the training agent: should be 3/3 storyboards clean (list_creatives, get_signals, get_media_buys).

Acceptance criteria

  • `npm run build:compliance` clean.
  • `npm run test:pagination-invariant` clean.
  • `pagination_integrity` filter run against the training agent: all storyboards clean.
  • Negative test: flipping `has_more` to false on the first page (or stripping the pagination block) trips the storyboard with the expected diagnostic.
  • Existing `get_media_buys` unit tests still pass (`server/tests/unit/training-agent.test.ts`).
  • Pre-existing `get_media_buys` callers in storyboards (`media-buy-lifecycle`, `sales-*` specialisms) keep working — they don't pass pagination today and shouldn't be affected by the addition.

Pointers

  • Pattern: `pagination-integrity.yaml` and `get-signals-pagination-integrity.yaml`
  • Cursor codec: `encodeOffsetCursor` / `decodeOffsetCursor` at `server/src/training-agent/task-handlers.ts:2087-2127`
  • Handler to modify: `handleGetMediaBuys` at `server/src/training-agent/task-handlers.ts:1642`
  • Schema: `static/schemas/source/media-buy/get-media-buys-response.json`
  • Storyboard authoring: `docs/contributing/storyboard-authoring.md`

Out of scope

  • Changing the query_summary structure of the response (just add the pagination envelope).
  • Cross-storyboard regressions in the sales-* specialisms (they shouldn't touch this path).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingclaude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions