Fix review findings: perplexity transport, URL corruption, version injection, docs - #23
Merged
Merged
Conversation
golangci-lint v2.9.0 is built with Go 1.25 and panics when type-checking against the Go 1.27 standard library, so the Lint job has been failing on main since 2026-08-24 and `mise lint` fails locally the same way. v2.13 renames two linters the config already disabled by their old names, so exhaustruct_v5 and gomodguard_v2 started firing; disable them the way wsl/wsl_v5 is already handled. Keep .mise.toml and ci.yml pins in sync. The remaining changes are what the new version reports: perfsprint's autofix in search.go, and goconst constants in two test files.
perplexity-search has never worked. Search built a resty request with no
method and no path and dispatched it with Send(), which resolves to
Execute("", ""): resty joined the empty path onto the base URL and Go
defaulted the empty method to GET, so every search fired
GET https://api.perplexity.ai/ instead of POST /chat/completions.
checkAPIError has no 404 case, so the wrong response parsed as valid and
the tool printed a blank answer and exited 0.
Do now takes an explicit method and path. Independently, the answer was
read from a top-level "answer" field that the API does not return; it
comes from choices[0].message.content, with ErrNoChoices when the slice
is empty. Sources now come from search_results (title + URL), falling
back to the deprecated citations field.
--max-results was accepted, passed to Search and discarded into `_ int`;
it now caps the source list. The default model sonar-medium-online was
retired by Perplexity and returns 400 — the default is now sonar. The
CLI also searched only the first argument, so unquoted multi-word
queries silently lost every word but the first.
Drops the dead apiKey field, the unused SearchOptions type and the
duplicate SetContext call. Package coverage 75.8% -> 91.9%.
url.Values.Get already returns the percent-decoded uddg value; the extra url.QueryUnescape decoded it a second time, so any target URL whose own form still contained %xx or + came back corrupted -- an inner %2520 became a literal space, and C%252B%252B became C++. The existing test only used a target with no inner encoding, so it passed. Also drops the dead IsEmptyResults/IsRateLimitPage parser methods and the orphaned rate_limit.html fixture, and uses the http5xxThreshold constant where a bare 500 had been left behind. FindRateLimitIndicator stays a deliberate no-op, per CLAUDE.md.
TestIsRateLimited never called isRateLimited -- it reimplemented the predicate inline as `status == 429 || status >= 500`, so it covered neither DuckDuckGo's 202 signal nor the transport-error branch, and could not have caught a change to the real function. It now calls the real one, with a companion httptest that drives Client.Do through a 202 retry. Adds the missing coverage for buildSearchParams (site/region/time/safe -search mapping), Searcher.calculateDelay (a separate, jitter-free implementation from the Client's) and Search's blank-query early return. Package coverage 70.4% -> 77.6%.
-ldflags -X patches variables, not constants, so `const version = "dev"` made the documented build-time injection silently impossible: all three binaries always reported "dev". The CI build step also passed no -ldflags and never built or smoke-tested perplexity-search at all. Also replaces ddg-search's query assembly -- args.First() plus a strings.Builder loop named querySb82 -- with strings.Join over all args.
Closes Djarvur#19: `go install github.com/Djarvur/ddg-search@latest` fails -- that path holds no main package. Install each command under ./cmd/ instead. The build-from-source block called `make build`, but there is no Makefile; it now shows the go build commands. The documented sonar-small/medium/pro-online models were retired by Perplexity and are rejected by the API, so both README and SKILL.md listed models that cannot work; replaced with the current sonar family. Neither the binary nor any dependency reads .env, so the docs promised key loading that does not happen -- they now say to export the variable. Also refreshes what --max-results does, the Sources sample output, and the matching openspec requirements.
Updates the golangci-lint pin, drops the Makefile note now that the README no longer claims one, records that the dead keyword-scanning parser helpers are gone, and documents the two contracts a future change could easily break again: extractURL must not decode twice, and perplexity.Client.Do needs an explicit method and path.
onokonem
self-requested a review
September 2, 2026 07:43
onokonem
approved these changes
Sep 2, 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.
Closes #19. Addresses 10 of the 14 findings in #8, plus several bugs found in the follow-up review attached to that issue.
Everything here is confined to
main. Nothing in this branch touches PR #10 orinternal/mcp.Why this is worth reviewing first
Two of these are not cleanups — they are "the tool does not do what it says":
perplexity-searchhas never worked. It sentGET https://api.perplexity.ai/and printed a blank answer with exit code 0.ddg-searchsilently corrupted result URLs whose target contained percent-encoding.Both are reproduced by tests added in this branch.
Real bugs
perplexity-searchnever reached the APISearchbuilt a resty request with no method and no path and dispatched it withSend(), which resolves toExecute(r.Method, r.URL)— i.e.Execute("", ""). resty joined the empty path onto the base URL, and Go'shttp.NewRequestdefaults an empty method toGET, so every search fired:checkAPIErrorhas no 404 case, so the wrong response parsed as a valid one: the tool printed an empty answer and exited 0.Donow takes an explicit method and path.TestSearchSendsPostToChatCompletionsasserts the method, path and body a real request carries.The answer was read from a field the API does not return
Independently of the above,
APIResponse.Answerwas mapped from a top-leveljson:"answer"field. Perplexity's OpenAI-compatible response puts the text atchoices[0].message.content, soAnswerwas always empty even against a correct endpoint. Fixed, withErrNoChoicesguarding an emptychoicesarray instead of printing nothing.Sources now come from
search_results(which carries titles), falling back to the deprecated top-levelcitationsarray when it is absent.--max-resultswas a no-opThe flag was declared, parsed, passed to
Searchand discarded into_ int. It now caps the source list. The flag is kept rather than removed becauseskills/perplexity-search/skill.yamlpasses it.The default model no longer exists
sonar-medium-online— along with the rest of thesonar-*-onlinefamily — was retired by Perplexity and is rejected by the API. Even with the transport fixed, the default would have failed with a 400. Default is nowsonar; the model lists in README and SKILL.md were updated to the current family.extractURLdecoded DuckDuckGo targets twiceu.Query().Get("uddg")already returns the percent-decoded value; the followingurl.QueryUnescapedecoded it a second time. Any target URL whose own form still contained%xxor+came back corrupted:The existing test used a target with no inner encoding, so it passed. Regression tests for both cases added.
--versioncould never be set-ldflags -Xpatches variables, not constants, soconst version = "dev"made the documented build-time injection impossible — all three binaries always reporteddev. Changed tovarin all three, and verified:$ go build -ldflags="-X main.version=v9.9.9-test" ./cmd/ddg-search $ ./ddg-search --version ddg-search version v9.9.9-testThe CI build step also passed no
-ldflags, and never built or smoke-testedperplexity-searchat all. Both fixed.Cleanups
ddg-searchassembled its query fromargs.First()plus astrings.Builderloop namedquerySb82; replaced withstrings.Join(args.Slice(), " ").Parser.IsEmptyResults/Parser.IsRateLimitPageand the orphanedtestdata/rate_limit.html.FindRateLimitIndicatorstays a deliberate no-op — the false-positive problem it works around is real.Client.apiKeyfield, the unusedSearchOptionstype and a duplicateSetContextcall ininternal/perplexity.http5xxThresholdconstant where a bare500had been left.Tests
TestIsRateLimitednever calledisRateLimited— it reimplemented the predicate inline asstatus == 429 || status >= 500, so it covered neither DuckDuckGo's 202 signal nor the transport-error branch, and could not have caught a change to the real function. It now calls the real one, with a companion httptest drivingClient.Dothrough a 202 retry.Also added the missing coverage for
buildSearchParams,Searcher.calculateDelay(a separate, jitter-free implementation from the Client's) andSearch's blank-query early return.internal/perplexityinternal/searchThree things that were not in the original findings
1. CI on
mainhas been failing since 2026-08-24, and not because of the code.golangci-lint v2.9.0is built with Go 1.25 and panics when type-checking against the Go 1.27 standard library:mise lintfails the same way locally, which made the repo's own mandatory check impossible to run. Bumped the pin tov2.13.2in both.mise.tomlandci.yml. v2.13 renamed two linters this config already disabled under their old names, soexhaustruct_v5andgomodguard_v2started firing — disabled them the waywsl/wsl_v5already is. If you would rather pin a different version, that is the one line to change; everything else in this branch is independent of it.2.
perplexity-searchsearched only its first argument.perplexity-search golang tutorialwithout quotes searched forgolang. This is the same defect as finding 1.2 inddg-search, which the review caught only on the DuckDuckGo side. Fixed, since the README documents exactly that unquoted invocation.3. The
.envsupport in the docs does not exist. Neither the binary nor any dependency reads.env— the key comes from the environment only. README, SKILL.md, the error message and the spec all promised loading that never happened. They now say to export the variable, with a note on how to source a.envyourself. Adding a dotenv dependency seemed out of scope for a fix branch; happy to do it instead if you would rather keep the documented behaviour.Docs and specs
README.mdalso had thego installpath from #19 and amake buildstep for a Makefile that does not exist.Because the default model and the stdout format changed,
skill.yamlandSKILL.mdare updated in the same change, per the repo's own rule. Theopenspec/specsrequirements for perplexity-search and rate-limit-handler are updated to match actual behaviour.CLAUDE.mdis added here as well — it documents the retry nesting, the deliberateFindRateLimitIndicatorstub, and the two contracts this branch fixes, so they are harder to reintroduce.Deliberately not included
cmd/*tests (all three are at 0%); better as its own change.(N+1)²retry compoundingCLAUDE.md; a user-facing note can follow separately.internal/mcpVerification