stringify_values/1 had a catch-all that coerced any non-map payload to
%{}, silently dropping all form fields on the files: :form path. Narrow
the catch-all to nil only (the legitimate payload || %{} case); any other
non-map now raises FunctionClauseError instead of vanishing on the wire.
Add prepare_body/3 form-field tests covering the map, nil, and
non-map-raises cases.
Adds `Dexcord.Api.Coverage`, a testable core that diffs the compiled
endpoint route table against a pinned snapshot of Discord's official
OpenAPI spec (`priv/discord_openapi.json`). The spec is a coverage
CHECKLIST, not codegen input: it is refreshed only by the deliberate
`mix dexcord.coverage.refresh`, so upstream churn never breaks CI
spontaneously.
- `mix dexcord.coverage` compiles then exits non-zero on any in-scope
endpoint missing from the declared surface (AC1.8/AC1.9).
- `mix dexcord.coverage.refresh` pins the spec via :httpc + castore,
adding inets/ssl to the code path since Mix prunes them.
- `priv/coverage_allowlist.exs` records out-of-scope spec routes with a
comment per family; reconciled against the real pin (Social SDK
lobbies/partner-sdk, OIDC, application-by-id, scheduled-event
exceptions, join requests, thread search, monetization).
- `.gitea/workflows/ci.yml` runs the suite + gate (Actions must be
enabled on the Gitea remote).
- ratelimit_test.exs gains AC1.10 spot-checks proving the new Phase 5
route shapes collapse to bounded, id-collapsed keys.
- [Critical] Facade delegate set is now derived from static, compile-time-stable
data (each group's __endpoints__/0 route table) instead of the group's
module_info(:exports). module_info/1 is an auto-generated function that the
Elixir compiler tracer does not record as a compile-time dependency, so no
compile-order edge was established between Dexcord.Api and the group modules;
under parallel/incremental compilation the export table was read incompletely,
silently dropping delegates (e.g. Dexcord.Api.get_gateway_bot/0 undefined after
a routine recompile). Arities are derived from segment param count + body flag
(each head's opts \\ [] yields base and base+1), with get_channel_messages/3
named explicitly as sugar. Verified: delegate always present in the compiled
beam; 8x incremental-recompile loop of facade + gateway_integration all green.
- [Important] api_facade_test.exs is now deterministic: it asserts the facade's
exported set equals the same statically-derived expected set the generator
uses (no module_info), and ensures Dexcord.Api + groups are loaded before
function_exported? probes (which do not auto-load), removing a false-negative
async load race. Historically-load-bearing arity assertions kept.
- [Minor] decode_response/2 now normalizes returns:nil endpoints to {:ok, nil}
even when the server sends a non-204 JSON body, instead of leaking the raw
decoded map through the passthrough clause. Added a unit test.
Redeclare the full existing endpoint surface with the Dexcord.Api.Endpoint
DSL across six group modules (Channels, Messages, Users, Interactions,
Commands, Misc) and delete the old named endpoint functions plus their
private helpers (message_query/maybe_param/maybe_put) from Dexcord.Api,
keeping request/4 and the rest of the request pipeline untouched (AC1.7).
Dexcord.Api re-exports every group endpoint head as a generated defdelegate
driven by module_info(:exports), so Dexcord.Api.<endpoint> keeps working for
all 9 internal call sites and external callers. create_dm is registered by
hand (spec + snowflake sugar) because the generated create_dm(body, opts \\ [])
head would collide with create_dm/1.
Endpoints are now typed: get_current_user -> Dexcord.User, create/edit_message
-> Dexcord.Message, get_channel/start_thread -> Dexcord.Channel,
get_current_application -> Dexcord.ApplicationInfo, bulk_overwrite_* ->
[Dexcord.ApplicationCommand], etc. get_gateway_bot and
create_interaction_response stay :raw. Patch registrar to match
%Dexcord.ApplicationInfo{} and widen Config.put_application_id/1 to accept
integer ids.
Behavior changes (documented in tests):
- get_channel_messages/2 no longer collapses multiple anchors with a
before>after>around precedence; it forwards every declared query key, so
callers pass exactly one anchor (the /3 locator arity still enforces this).
- start_thread_with_message extra fields now travel in the body argument
rather than a trailing opts list.
- *_id path params are validated via Snowflake.cast/1; integration-test
fixtures updated from non-numeric placeholders to numeric snowflake ids.
Add test/dexcord/api_facade_test.exs asserting the facade cannot silently drop
any group endpoint export.
Claude-Session: https://claude.ai/code/session_01T4vgXtQLs2wfNwn6B1m4hm
- Formatting: add field/hydrate/discord_struct to locals_without_parens
in .formatter.exs so the no-paren data-DSL style is the formatted form;
mix format then wrapped the long codec encode_value/2 clause. Both
previously-unformatted files now satisfy mix format --check-formatted.
- Empty-struct guard: __before_compile__ now raises ArgumentError when a
module does 'use Dexcord.Struct' with no field/hydrate declarations,
instead of silently generating a bare defstruct []. Added a compile-time
test mirroring the existing Code.compile_string validation tests.
- Api.get_channel_messages/2,3 (opts + Nostrum-compatible locator), query
baked into the path so the rate limiter buckets it correctly
- Api.start_thread_with_message/3,4 (POST .../messages/:id/threads)
- Dexcord.Snowflake: from_datetime/to_datetime/to_unix (replaces Nostrum.Snowflake)
- Cache.threads/1: thread-type channels (10/11/12) for a guild
- FakeRest now records query_string so query building is testable
Added to support migrating Alamedya off Nostrum. 178 tests, 0 failures.
- Tolerate op-0 frames with a non-binary `t` (e.g. `t: null`): the gateway
no longer crashes the gen_statem via EventNames.to_atom/1's is_binary guard;
such protocol noise is logged at debug, still bumps seq, and is not dispatched.
- Session.establish/2 resets last_seq to nil so a new session never inherits the
previous session's sequence; READY's own frame reseeds it. Also stop bumping
last_seq from frames replayed under reidentify_pending? (abandoned session).
- queue_or_send now follows queue-first FIFO discipline: a frame is only sent
immediately when the queue is empty, otherwise it falls in behind existing
queued frames and the front is drained, preventing newer op-3/op-8 frames from
jumping older ones.
- op-3/op-8 casts outside :connected are enqueued (bounded) instead of dropped,
so member-chunk requests issued during :resuming replay are not lost; they
drain on the next :connected enter. Dropped only with no session context.
- Refund the token on a failed budgeted send (no more double-charge) and extract
the shared send_one_budgeted/2 helper used by both the send-now and drain paths.
- Document the reidentify_pending? invariant at its declaration.
- Registrar: restart: :temporary + bounded in-process retry (2s/10s, configurable)
so a failing registration never cycles the gateway or shares the tree's
max_restarts budget; loud error + normal exit on give-up.
- Global mode: skip the overwrite when commands/0 is empty (symmetric with guild
mode) so a boot never wipes production commands; log how to wipe intentionally.
- Slash: route on top-level interaction type - 2 -> handle_interaction (by name),
3 -> handle_component, 5 -> handle_modal (by custom_id). New optional callbacks
with use-injected debug catch-alls; modules defining only handle_interaction
still compile and work.
- Supervisor: nest Cache + Dispatcher under a :rest_for_one supervisor so a Cache
crash recreates the ETS tables and restarts the Dispatcher after it, leaving
the outer one_for_one Gateway/Session semantics untouched.
- Slash response helpers: merge a caller-supplied integer flags with the
ephemeral bit via Bitwise.bor (4 -> 4; 4 + ephemeral -> 68).