Repository navigation
feat(article): a MediaWiki client with nothing implicit - #51
Willi363363 wants to merge 1 commit into
Conversation
Step 3.2 of phase 3. Language and user agent are parameters on every call, and the type will not let a caller omit them. The defect this closes is worse than the phase sheet describes. The sheet says the flag-report checker "silently queries Wikipedia in another language depending on call order"; the code says `flag_verifier.py` never sets either global at all. Only `scraper.py` does. So on a freshly restarted process, before any game has been generated, a report about a French article is fact-checked against the **English** Wikipedia — with the library's default user agent, which Wikimedia's policy refuses. Recorded as D13, along with the `auto_suggest` left on in the same function, which can resolve a search to a different article than the one asked for. Failure is a value with a closed set of reasons, because a caller has to tell "this topic does not exist" from "Wikipedia is down": the first means try another topic, the second means stop trying. The current code wraps three nested `try` blocks around that distinction and then loses it to a bare `except Exception`. The transport is injected, so no test touches the network — and, more useful, every test can read the URL that was built. The bug being prevented is a request going to the wrong Wikipedia, and the only way to check that is to look at where the request went. Verified biting: reintroducing a remembered language makes the leakage test fail, and adding a search parameter to the page lookup fails the no-auto-suggestion test. The language reaches a hostname, so it is validated rather than interpolated: `fr.evil.example` and `fr/../en` are refused, and nothing is sent. Same for a blank user agent — a refused request must not reach Wikipedia at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01533WTTbgmfCaDP5tmK8Gkv
PR Summary by QodoAdd explicit MediaWiki client with per-call language/user-agent and typed failures
AI Description
Diagram
High-Level Assessment
Files changed (7)
|
Code Review by Qodo
1. Search hits shape crash
|
| const titles = hits | ||
| .map((hit) => hit.title) | ||
| .filter((title): title is string => typeof title === 'string'); | ||
|
|
There was a problem hiding this comment.
1. Search hits shape crash 🐞 Bug ☼ Reliability
searchTitles() assumes query.search is an array and calls .map() without runtime validation, so a JSON response where query.search is non-array will throw instead of returning a typed unexpected_response failure. This breaks the intended contract that even bad/changed API responses should be handled as Result failures rather than exceptions.
Agent Prompt
### Issue description
`searchTitles()` casts the JSON body to `SearchBody` and then assumes `query.search` is an array, calling `.map()` directly. Since the JSON is untrusted at runtime, `query.search` can be a non-array value (or even `null`) and this will throw, defeating the purpose of returning `Result` failures instead of exceptions.
### Issue Context
This module is explicitly designed around “failure as a value” (typed reasons like `unexpected_response`). Any runtime throw from parsing/shape assumptions undermines that goal.
### Fix Focus Areas
- packages/article/src/mediawiki.ts[119-129]
### Implementation notes
- After reading `hits`, validate with `Array.isArray(hits)`.
- If it is not an array, return `failed('unexpected_response', 'query.search was not an array')` (or similar).
- Optionally also validate that each hit is an object before reading `hit.title` (to avoid crashes on non-object items).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (answer.error !== undefined) { | ||
| // `missingtitle` is the ordinary "no such page"; anything else is a problem | ||
| // with the request rather than with the topic. | ||
| return answer.error.code === 'missingtitle' | ||
| ? failed('not_found', `no page titled ${trimmed}`) | ||
| : failed('unexpected_response', `API error ${String(answer.error.code)}`); |
There was a problem hiding this comment.
2. Null error/parse throws 🐞 Bug ☼ Reliability
fetchRenderedPage() dereferences answer.error.code and parsed.title/revid/text without guarding against null or non-object values, so certain JSON responses can still throw instead of returning unexpected_response. This can crash the process in exactly the scenarios this client is trying to handle gracefully (API changes, partial outages, or edge-case responses).
Agent Prompt
### Issue description
`fetchRenderedPage()` assumes `answer.error` (when present) is an object with a `code` property, and assumes `answer.parse` (when present) is a non-null object before reading `parsed.title`, `parsed.revid`, and `parsed.text`. Because the body is untrusted JSON, `error`/`parse` can be `null` or another type, causing runtime exceptions.
### Issue Context
The design intent is to return `Result` failures for malformed/unexpected API responses (`unexpected_response`) rather than throwing.
### Fix Focus Areas
- packages/article/src/mediawiki.ts[168-185]
### Implementation notes
- Validate `answer.error` with something like:
- `if (answer.error != null && typeof answer.error === 'object' && 'code' in answer.error) { ... }`
- otherwise, if `answer.error !== undefined` but not the expected shape, return `failed('unexpected_response', 'API error block had unexpected shape')`.
- Validate `parsed` similarly:
- ensure `parsed != null && typeof parsed === 'object'` before reading properties.
- Consider also accepting `revid` as a numeric string if MediaWiki ever serializes it that way (optional), otherwise keep strict number but return a failure, not a throw.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Closed by the stack collapse, not abandoned. Every commit of this pull request is in Three commit messages were reworded on the way — a This description and its review thread stay readable here. |
Step 3.2 of
plans/rewrite/phase-03-article.md.Stacked on #50. Base is
feat/rewrite-phase-3-parity, so the diff here is step3.2 alone.
The defect is worse than the sheet says
The sheet says the flag-report checker "silently queries Wikipedia in another
language depending on call order". The code says something sharper:
flag_verifier.py:27never sets either global at all. Onlyscraper.py:96does.
So on a freshly restarted process, before any game has been generated, a report
about a French article is fact-checked against the English Wikipedia — with
the
wikipedialibrary's default user agent, which Wikimedia's policy refuses.Not "depending on call order": English until the first game.
The same function also calls
wikipedia.page(results[0])withoutauto_suggest=False, so a lookup can resolve to a different article than the onesearched for.
Recorded as D13 in the debt register and the contract's defect list. That
matters beyond bookkeeping: the D list is what gates entry into phase 10, and a
defect the plan mentions only in a phase sheet has nothing checking it was
closed.
Language and user agent, per call
There is no global to be wrong. The type will not let a caller omit either:
The language reaches a hostname, so it is validated rather than interpolated
—
fr.evil.exampleandfr/../enare refused and nothing is sent. Same for ablank user agent: a refused request must not reach Wikipedia at all, which the
test asserts by checking no URL was recorded.
Failure as a value, with reasons that differ
A caller has to tell "this topic does not exist" from "Wikipedia is down" —
the first means try another topic, the second means stop trying. The current code
wraps three nested
tryblocks around that distinction and then loses it to abare
except Exception.429is its own reason rather than a generic error, so a retry loop can back offinstead of hammering Wikimedia.
The transport is injected
No test touches the network — and, more useful than that, every test can read
the URL that was built. The bug being prevented is a request going to the wrong
Wikipedia, and the only way to check that is to look at where the request went:
Verified biting on both defects reproduced:
sends each call to the wiki it was asked forasks for the exact title, and follows redirects onlyRedirects are followed, and that is not auto-suggestion: a redirect is a page
saying where it moved, not a guess about what you meant.
Checks
No new dependency — the client uses
fetch.One typing note: the test's fake transport is typed by
typeof globalThis.fetchrather than by naming
RequestInfo, which is not available without a DOM lib thispackage does not load.
Not run: the Python backend tests — no
pytesthere. This touches no Python.Next
3.4 (falsification via
generateObject), 3.5 (end-to-end parity), 3.6 (Rediscache), 3.7 (counters).
🤖 Generated with Claude Code
https://claude.ai/code/session_01533WTTbgmfCaDP5tmK8Gkv