Skip to content

fix: route unmapped colony slugs through ?colony= instead of ?colony_id=#45

Merged
jackparnell merged 4 commits intomainfrom
fix/colony-sdk-slug-resolution
Apr 28, 2026
Merged

fix: route unmapped colony slugs through ?colony= instead of ?colony_id=#45
jackparnell merged 4 commits intomainfrom
fix/colony-sdk-slug-resolution

Conversation

@ColonistOne
Copy link
Copy Markdown
Collaborator

Summary

get_posts(colony=<slug>) and search_posts(colony=<slug>) previously used COLONIES.get(colony, colony) which silently fell through to ?colony_id=<slug> for any slug not in the hardcoded map. The API's UUID validator then rejected the request with HTTP 422 — silently breaking any caller that round-robined across newer sub-communities.

The fix adds _colony_filter_param(value):

  1. Known slug (in COLONIES) → canonical UUID under colony_id.
  2. UUID-shaped value → passes through as colony_id.
  3. Otherwise → routes under colony for server-side resolution.

The Colony API exposes both ?colony_id=<uuid> and ?colony=<slug> for filtering, so this is a clean fix that doesn't require a slug→UUID round-trip.

Where it bit

Caught while reviewing Langford (the LangGraph dogfood agent) — its engage loop round-robins through findings,meta,builds,general and every builds cycle was 422'ing:

WARNING colony_sdk: ← GET https://thecolony.cc/api/v1/posts?colony_id=builds → HTTP 422
WARNING langford: engage: get_posts(builds) failed: Colony API error...

Same pattern would hit any new sub-community the platform adds (lobby, imagining, etc.).

Out of scope

create_post, join_colony, leave_colony post the colony reference in a body field or URL path that the API only accepts as a UUID. Calls there with an unmapped slug still error. Fixing those requires a slug→UUID lookup against list_colonies; tracked for a follow-up rather than bolted into this PR.

Test plan

  • 5 new tests in test_client.py::TestColonyFilterParam:
    • known slug → UUID under colony_id
    • UUID passthrough (lower + upper case)
    • unmapped slug (builds, lobby, imagining) → colony param
    • async client import wiring
  • Full suite passes locally: 416 tests in 4.92s.
  • CI-side checks (lint, typecheck, multi-Python tests).

Re: PR #42 (open, version bump to 1.8.1)

Left this PR's pyproject.toml version untouched and added an "Unreleased" section in CHANGELOG.md. Whichever PR lands first owns 1.8.1; the other rebases to 1.8.2.

`get_posts(colony=<slug>)` and `search_posts(colony=<slug>)` previously
used `COLONIES.get(colony, colony)` which silently fell through to
`?colony_id=<slug>` for any slug not in the hardcoded map. The API's
UUID validator then rejected the request with HTTP 422.

The Colony API exposes both `?colony_id=<uuid>` and `?colony=<slug>`
for filtering; route unmapped slugs through the slug-friendly param.
The new `_colony_filter_param(value)` helper picks the right pair:

  1. Known slug -> canonical UUID under `colony_id`.
  2. UUID-shaped value -> passes through as `colony_id`.
  3. Otherwise -> `colony=<slug>` (server resolves).

Symmetric fix in AsyncColonyClient. 5 new regression tests cover
known-slug, UUID passthrough (lower + upper), unmapped-slug routing,
and async-client import wiring.

Caught while Langford (LangGraph dogfood agent) was round-robining
through findings/meta/builds/general; every `builds` cycle 422'd:

  WARNING colony_sdk: GET /api/v1/posts?colony_id=builds -> HTTP 422
  WARNING langford: engage: get_posts(builds) failed

Out of scope: `create_post`, `join_colony`, `leave_colony` use the
colony reference in a body field / URL path that the API only accepts
as a UUID; those still need a slug->UUID lookup against
`list_colonies`. Tracked for a follow-up.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@jackparnell jackparnell merged commit f9292ae into main Apr 28, 2026
7 checks passed
ColonistOne added a commit that referenced this pull request Apr 28, 2026
* fix: close slug-resolution gap on create_post / join / leave

PR #45 fixed the filter call sites (`get_posts`, `search_posts`) by
routing unmapped slugs to the API's slug-friendly `?colony=` query
param. The body/URL-path call sites couldn't use that workaround —
the API only accepts a UUID for `body.colony_id` and
`/colonies/{colony_id}/{join,leave}`.

This change adds `_resolve_colony_uuid(value)` to both `ColonyClient`
and `AsyncColonyClient`:

  1. Known slug (in `COLONIES`) -> canonical UUID.
  2. UUID-shaped value -> passthrough.
  3. Unmapped slug -> lazy `GET /colonies?limit=200`, cache the
     slug->id map on the client, look up the slug.
  4. Truly-unknown slug -> `ValueError` with sample of available
     names. Distinguishes a typo from a transient API failure.

Cache populated on first miss against `COLONIES`; never invalidated
for the lifetime of the client (sub-communities are stable). Each
client gets its own `_colony_uuid_cache` instance attribute.

7 new regression tests (`test_client.py::TestResolveColonyUuid`)
cover known-slug fast path, UUID passthrough, lazy lookup,
cache reuse, ValueError on truly-unknown, dict-vs-list response
shape tolerance, and async-mirror existence.

Closes the "out of scope" loose end from PR #45. With this landed,
the SDK is fully slug-aware across every call site that takes a
colony reference.

Full suite still passes: 423 tests in 4.60s.

* fix: tolerate _raw_request's {data: [...]} bare-list wrapping

_raw_request wraps non-dict JSON responses in {"data": parsed} (line
~620 in async_client.py). The /colonies endpoint returns a bare list,
so the resolver's response-shape sniffer never found the items —
it checked for {items: ...} / {colonies: ...} envelopes but missed
the actual {data: [...]} shape used here.

Adds {data: [...]} to the response-shape tolerance list, plus 6 new
async regression tests using httpx.MockTransport (mirror of the sync
TestResolveColonyUuid). Covers known-slug fast path, UUID passthrough,
lazy lookup via list_colonies, cache reuse, ValueError on unknown,
dict-envelope tolerance.

Coverage: 100% on both client.py and async_client.py (1187/1187 lines).

* fix(lint): apply ruff format to test_async_client.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants