Skip to content

bug(sdk): three SDK methods send query params the routes do not accept, so pagination and period are ignored #15119

Closed

Description

@mrveiss

Problem

Three SDK methods send query parameters the route does not accept. AutoBotClient.get() drops
None values but passes everything else through (libs/autobot-sdk-python/autobot_sdk/client.py:116-121),
so the parameter is sent, ignored by FastAPI, and the caller's intent silently does not apply.

1. Pagination never advances. resources/knowledge.py:57 sends offset=:

raw = await self._c.get("/knowledge_base/entries", limit=limit, offset=offset, category=category)

GET /api/knowledge_base/entries (autobot-backend/api/knowledge.py:1669) declares cursor,
not offset (api/knowledge.py:1678) — the route is cursor-paginated. Every call therefore
returns the first page regardless of the offset passed, and the route's real next_cursor
is not modelled by the SDK at all.

2/3. Analytics time window is ignored. resources/analytics.py:25 and :29 both send
period=. Neither get_usage_statistics (autobot-backend/api/analytics.py:369) nor
get_performance_metrics (api/analytics.py:288) declares such a query parameter, so both
always return whatever their default window is.

Acceptance criteria

  • knowledge.entries() paginates: a second call with the cursor from the first returns
    different rows, asserted against a stubbed route rather than by inspection.
  • The SDK surfaces next_cursor so a caller can page at all.
  • The two analytics methods either send a parameter the route accepts, or stop advertising
    a period argument they cannot honour.
  • A guard asserts each SDK-sent query parameter name exists on the route signature it
    targets, so the next one added fails CI instead of being ignored at runtime.

Notes

Found while delivering #15072, alongside #15114, #15116 and the field-name drift issue. Those
three are response-shape drift; this one is request-shape drift, which the response contract
guard added by #15072 does not cover.

Activity

  1. added
    bugSomething isn't working
    pythonPull requests that update python code
    on Aug 26, 2026
  2. github-actions commented on Aug 28, 2026

    @github-actions
    Contributor

    PR #15137 (merged to Dev_new_gui) references this issue with a close keyword.

    fix(sdk): stop sending query parameters the routes do not accept (#15119)

    If this issue is fully resolved, close it manually. If work remains, no action is needed.

  3. mrveiss commented on Aug 28, 2026

    @mrveiss
    OwnerAuthor

    Verified against merged fc3a93aa4 — three criteria met, AC4 partially. NOT closing.

    PR #15137 merged as fc3a93aa4. Closes does not fire on Dev_new_gui, so this was open mechanically; verified per criterion rather than assumed.

    # Criterion Verdict Evidence
    1 knowledge.entries() paginates, asserted against a stubbed route MET repo_tests/sdk_request_url_test.py:329-360 — MockTransport keyed on the cursor param, asserts seen_cursors == ["0","17"] and that the second page returns different rows. Fix at libs/autobot-sdk-python/autobot_sdk/resources/knowledge.py:61,75
    2 The SDK surfaces next_cursor so a caller can page at all MET libs/autobot-sdk-python/autobot_sdk/models.py:319, consumed at sdk_request_url_test.py:352,357
    3 The two analytics methods send an accepted parameter or stop advertising period MET resources/analytics.py:34,39,42,44 — both now send no query params; routes declare none (autobot-backend/api/analytics.py:288,369)
    4 A guard asserts each SDK-sent query parameter exists on the route it targets, so the next one added fails CI PARTIAL see below

    Why AC4 is not fully met

    The guard is real and sound for what it checks — the oracle is built with get_openapi and is protected against a vacuous pass at sdk_request_url_test.py:274-278, and repo_tests is a gating pytest root (.github/workflows/ci.yml:287).

    But its input is the pinned request rows, not the SDK's method signatures. So:

    • a new query parameter with a non-None default is sent by every call, appears in the observed set, and is caught;
    • a new query parameter defaulting to None is dropped by AutoBotClient.get() when no row passes it, so the observed set is unchanged and the guard passes — while real callers who do pass it send a parameter the route ignores.

    Most optional parameters default to None, so the case the criterion is about is the case the guard misses. AC4's operative clause is "so the next one added fails CI"; for the common shape, it does not. Filed as #15187, and this issue stays open until that closes.

    Two things this verification surfaced

    Refs #15137, #15186, #15187.

  4. mrveiss commented on Aug 29, 2026

    @mrveiss
    OwnerAuthor

    Picked this up expecting the three-method fix described in the body to be unstarted; found it already merged in PR #15137 and this issue's remaining scope narrowed to AC4, tracked separately as #15187. Delivered that in PR #15265 (signature-derived query-parameter guard + contrast-mutation test), rather than duplicating #15137's work. Once #15265 lands and #15187 closes, this issue should be re-checked for closure per the earlier verification comment's own condition.

  5. mrveiss commented on Aug 29, 2026

    @mrveiss
    OwnerAuthor

    AC4 now holds — closing

    The earlier verification comment recorded three of four criteria met and this one only partially, then held the issue open on it. That remainder was split out as #15187 and is now merged as 965f21c80 (PR #15265).

    # Criterion Verdict
    1 knowledge.entries() paginates, asserted against a stubbed route MET (recorded previously)
    2 The SDK surfaces next_cursor MET (recorded previously)
    3 The analytics methods stop advertising a parameter the route does not accept MET (recorded previously)
    4 A guard asserts each SDK-sent query parameter exists on the route signature, so the next one added fails CI NOW MET

    AC4 was the interesting one, and it was not met by the original guard for a reason worth keeping on the record: that guard's input was the pinned rows, not the code. A parameter defaulting to None is dropped by the client when no row passes it, so it never reached the wire to be checked — the guard passed while a real caller who did pass it sent something FastAPI ignores. "The next one added fails CI" was true only for non-None defaults, which is the minority.

    The strengthened guard derives its parameter set from inspect.signature and typing.get_type_hints and force-calls each method so every optional parameter reaches the wire regardless of the rows — while still separately requiring that a pinned row exercise each name, so one hole was not traded for another.

    Explicitly not covered, so nobody assumes it is: #15186's class — a parameter the route accepts and then ignores. It exists on the signature, so it passes a name-existence check. That is a behavioural defect and needs a different kind of test; it is stated in the guard module itself rather than left to inference.

    All four criteria met against merged code. Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    apibackendbugSomething isn't workingpythonPull requests that update python code

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions