feat(tendlc): 10DLC Registration Center foundation (PR 1 of 5) - #27
Open
kshahbw wants to merge 22 commits into
Open
feat(tendlc): 10DLC Registration Center foundation (PR 1 of 5)#27kshahbw wants to merge 22 commits into
kshahbw wants to merge 22 commits into
Conversation
…ssertion Go 1.23+ made timer channels unbuffered; Stop() cancels any in-flight send, so there is never anything left in the channel to drain after Stop() returns. The old drain-after-Stop idiom was dead code on this toolchain and would deadlock if it were ever reached. Create a fresh timer per iteration instead and only Stop() it on the cancellation path as a courtesy; unreferenced timers are garbage collected. Also fix TestPollRespectsContextCancellation: the calls == 0 assertion could never fail since Check always runs at least once before context cancellation can be observed. Assert calls > 1 instead, which actually exercises loop iteration before interruption.
…ract List() no longer treats null/absent data as an empty result: production sends data:[] for genuinely empty lists, so nil is a malformed response, not zero results, and must error like any other shape mismatch. Truncated's parameter is renamed to returnedSoFar and documented as the cumulative count across all pages walked, not the current page's length, to prevent downstream callers from misreading it on the last page of a paginated walk. Shape-mismatch errors now describe the JSON shape (array/object/null) instead of printing a Go type name.
Transport errors (connection refused, DNS, TLS, timeout) never unwrap to *api.APIError, so the status command fell through to the error path without emitting anything on stdout. Every probeErr path now defaults to the unknown/probe_failed result before checking for a more specific outcome, so callers parsing stdout always get stable JSON regardless of how the probe failed. Adds command-level tests exercising RunE against stub servers, covering the success path, both 403 outcomes, a 5xx, and the transport-failure regression.
…rrect stale comments
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
This was referenced Aug 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
BLUF
Foundation for 10DLC Registration Center support — PR 1 of 5. No write operations and no user-visible flags: this lands the shared plumbing that PRs 2–5 need, plus one new command (
band tendlc status).Design:
docs/specs/2026-08-12-tendlc-direct-registration-center-design.md· Plan:docs/plans/2026-08-13-tendlc-pr1-foundation.mdBoth live under a gitignored
docs/, so they are not in this diff.What's in it
New packages
internal/tendlc— typed service:ListBrands,GetBrand,ListCampaigns,GetCampaigninternal/customerprofile— typed service:List,GetShared plumbing added to
internal/apiEnvelope/Pageparsing withParseEnvelope,List(),Object(),Truncated()deepObjectquery encoding (Filter,EncodeQuery) — the API wantsstatus[eq]=REGISTERED; the plain form is accepted and then silently ignored, so the tests assert exact query stringsAPIError.Header+RetryAfter()so 429s can eventually be honoredExisting code, additively extended
cmdutil.FlagErrormakesExitFlagError(6) reachable — it was defined but nothing mapped to it, so flag errors exited 1PollConfig.Contextmakes--waitloops cancellable between attempts. All 12 existingPollcallers are untouchedband auth statusgains atendlctri-state and acustomer_profilesboolean.campaign_managementkeeps its exact existing meaningNew command
band tendlc status— one call, reports Registration Center accessDeliberately not here
Option structs,
--all, filter flags, read-modify-write,--confirm, receipts, and every write method. They land with their first consumers in PRs 2–5; scaffolding without callers would inflate this PR without making it useful.Notable decisions
Responses are
map[string]any, not typed structs. Production returns fields the published spec omits —attMessageClass,subId,universalEin,referenceId— andencoding/jsonsilently drops unknown fields into structs. Requests will be typed; responses stay lossless.band tendlc statusreports access only, never account mode. Direct-vs-import is a property of the customer's Bandwidth setup, not a runtime fact. Live probing killed the obvious inference:brand.importedistrueon both a direct and an import account.modeis always present and alwaysunknown, because omitting it invites callers to assume a default.A 403 exits 0. The probe answered its question. Only a probe that fails to answer exits non-zero — and it still emits stable JSON on stdout first, including on bare transport failures.
Verification
go build ./...,go test ./...,go vet ./..., and the doc-contract test all pass.knownDriftuntouched.Live-verified against a direct account (9901287) and an import account (9901303):
AGENTS.md's documented example matches that output byte for byte.
Known gaps, deliberately deferred
Test coverage:
RetryAfter's nil-Headerbranch is untested (the test useshttp.Header{}, not nil)client.goPostXMLReturnLocationheader capture has no regression test; onlydoRawis coveredRawQuery == ""Belongs to the flag layer (PRs 3–5):
Filter.Valueis skipped, so it cannot express "filter on empty" vs "unset"limit/offsetsilently vanish instead of erroringCosmetic:
tendlcCapability's doc comment doesn't enumerate the future probe's full reason vocabulary the waysipCapability's doesTenDLCfield shiftsError's JSON key order — irrelevant to parsersFound along the way, out of scope
Two real bugs in
internal/sip, both pre-existing and left alone rather than smuggled into this PR:internal/sip/service.go:94builds anapi.APIErrorwith a*RawResponsein scope and discards its headers, soRetryAfter()is always false for SIP errorscmd/sip/status.gohas the same transport-error-produces-empty-stdout gap that this PR fixes intendlcRelated
auth statusreportsauthenticated: truefor invalid credentials; token-exchange 401 exits 1 instead of 2context.Contextthroughapi.Clientand theRequesterinterfaceA running log of spec-vs-production divergences found while building this is at
docs/specs/2026-08-13-tendlc-spec-vs-prod-mismatches.md, to be filed with the tendlc API team once the stack lands.