Repository navigation
feat(search): paginate search results with --pages N - #30
Conversation
fancyboi999
left a comment
There was a problem hiding this comment.
Requesting changes on 2a59a4a (reviewed as the feature-only delta from #28).
The pagination direction matches #29, but two runtime paths can still lose or truncate results.
[P1] Preserve already-fetched pages when a mid-walk Playwright operation fails.
The pagination-state, page-change, and extraction page.evaluate() calls are outside the click-only try block. If the SPA destroys the execution context during a page transition, the exception propagates and the caller loses the first-page results. This contradicts the stated contract that mid-walk failures return partial results. Please catch the relevant Playwright/timeout failures around the complete per-page transition and return the accumulated items with a warning/termination reason.
[P1] Use the actual current DOM first card as the next page-change anchor.
After deduplication, cur_first_id = _first_card_id(fresh) stores the first new item rather than the page's actual first card. If page 2 starts with an item repeated from page 1, the stored anchor differs from the DOM before page 3 is clicked. _WAIT_PAGE_CHANGE_JS can then resolve immediately on the unchanged page; extraction reads page 2 again, finds no fresh items, and stops before page 3.
Set the anchor from the unfiltered next-page payload, check the page-change wait result, and add deterministic tests for a repeated first card, wait timeout, mid-walk exception, cross-page limit, deduplication, and pages_fetched/pages_total metadata.
Local verification at this exact head: 190 passed, Ruff clean, npm package tests 3/3, pack check clean, and git diff --check clean. None of the current automated tests executes _run() or the pagination state machine.
Because this PR is stacked on #28, it should be rebased onto the corrected #28 head after that review is addressed.
fancyboi999
left a comment
There was a problem hiding this comment.
Requesting changes on dbb757e after reviewing the correction delta from 2a59a4a.
The two original pagination findings are substantially addressed: per-page exceptions preserve accumulated results, the transition anchor now uses the unfiltered page payload, and the deterministic state-machine coverage is much stronger. Three items remain before this stacked PR can be merged.
[P1, blocking] Rebase onto the merged #28 cookie-records implementation.
This branch still descends from the original bf9f4bf and does not contain #28's final 7dad6ab / merged main state. The current tree removes cookie_types.py and restores the previously rejected cookie behavior: flat storage, QR provenance loss, and broadcasting cookies to both .taobao.com and .goofish.com with a forced / path.
#28 is now squash-merged as a536c479. Please rebase this PR onto current main, resolve browser.py/search conflicts by retaining the merged records/domain/path architecture, and ensure the resulting feature-only diff contains only pagination work.
[P2] Define behavior for cards without a parseable numeric item ID.
fresh filters by _item_id_from_url(...) not in seen, but IDs are added to seen only when non-empty. Two pages containing the same ...?id=abc card therefore both append it. I reproduced a three-page walk returning URLs 100, abc, abc with pages_fetched=3.
Please either skip cards without the required stable item ID or use an explicit stable fallback key, then add a cross-page regression for this shape.
[P2] Treat a non-dict pagination-state result as a graceful mid-walk error.
The first page.evaluate(_PAGINATION_JS) is exception-wrapped, but its result is not type-checked before pag.get(...). Returning None or another unexpected shape raises AttributeError and discards the partial-result contract. I reproduced this directly. Add the same isinstance(..., dict) guard used for extraction and return stopped_reason="error" with accumulated items.
Current isolated gates are otherwise green: 202 passed, 0 skipped/warnings, Ruff, npm tests 3/3, pack check, and git diff --check. I did not run live-cookie pagination E2E against this head because its stale #28 base still contains the rejected cross-domain cookie broadcast. After the rebase and the two boundary fixes, I will run --pages 3, exact cross-page limit, uniqueness/metadata, and partial-failure verification before approval.
The goofish search page is not infinite scroll: scrolling to the bottom stops growing cards, and pagination lives in a search-pagination- container (up to 50 pages, 30 cards each). Clicking the right arrow re-renders in place (URL unchanged) and ?page=N URL params are ignored, so `search items` could only ever return page 1. - add `--pages N` (default 1, capped at 50): click through the pagination control and accumulate cards across pages, dedup by item_id - repurpose `limit` as the cross-page total cap (MAX_LIMIT 200), keeping the old default of 20 so pages=1 behaves as before - stop conditions: --pages reached, right arrow disabled (last page), limit reached, or a page renders zero new cards (defensive) - wait for SPA re-render by watching the first card id change instead of a fixed sleep, with a 2s settle fallback - return pages_fetched / pages_total metadata alongside items
…ge-change anchor Address review P1s on 2a59a4a: - Extract the pagination loop into _walk_pages() and wrap the full per-page transition (pagination state read, arrow click, page-change wait, extraction) in a per-page try block. A destroyed execution context or any other Playwright failure now degrades gracefully and returns the accumulated partial results instead of raising, matching the stated contract. - Set the page-change anchor from the unfiltered next-page payload first card (= the actual DOM first card) instead of the deduplicated fresh list. A repeated first card across pages no longer desyncs the wait predicate, which could resolve immediately on an unchanged page and silently truncate the walk before page 3. - Check the page-change wait result: a timeout with fresh items continues (slow render), a timeout with no new items stops as "stale". - Add stopped_reason metadata (pages_reached / last_page / limit / no_new / stale / error / blocked) so callers can tell a normal finish from a truncated walk. - Add deterministic fake-page tests for the state machine: repeated first card anchor, wait timeout (both outcomes), mid-walk exception, cross-page limit, dedup, blocked page, non-dict payload, and the new metadata fields.
… numeric item id Address the two P2s from the 2026-09-02 review, on a rebased base: - Rebased onto a536c47 (fancyboi999#28 squash-merged): the feature-only delta now contains pagination work only and keeps the merged cookie-records architecture (cookie_types.py, domain/path provenance) intact. - Type-check the pagination-state evaluate result like the extraction result. A None/non-dict return no longer raises AttributeError and discards accumulated items; it stops gracefully with stopped_reason="error". - Skip cards whose URL has no parseable numeric item id on every page, not just cross-page dedup. The same "?id=abc" card can no longer be appended once per page; item_id remains a stable output key. - Add regressions: a bad-id card appearing on two consecutive pages is appended zero times, and non-dict pagination-state payloads (None/list/str/int) each end the walk as "error" with partial results preserved.
dbb757e to
2eb912f
Compare
fancyboi999
left a comment
There was a problem hiding this comment.
Requesting one final correction on 2eb912f after the rebase, full review, and successful real multi-page CLI validation.
The pagination work itself is now verified:
- GitHub checks: all 5 successful;
- local suite:
210 passed, 0 skipped/warnings; - Ruff, npm tests
3/3, pack check, andgit diff --check: clean; - real authenticated
X570 --pages 3 --limit 200: 90 items / 90 unique IDs / pages3/50/ ranks 1–90 /stopped_reason=pages_reached; - real authenticated
X570 --pages 2 --limit 45: exactly 45 unique items / pages2/50/stopped_reason=limit; - real authenticated
X570 Taichi --pages 5: 29 unique items / pages1/1/stopped_reason=last_page.
The previous partial-result, anchor, non-dict pagination, invalid-ID, limit, and metadata findings are addressed. One integration regression remains.
[P1, blocking] Preserve #28's exact auth-wall retry predicate after the rebase.
Merged main (a536c479) introduced _should_retry(payload) so the first-page retry runs only for items=[] together with requiresAuth=true. This PR deletes that helper and restores the broader condition that retries every zero-item state not marked empty or blocked.
I reproduced an unknown zero-item payload with requiresAuth=false: the current PR navigates twice before raising GoofishError. The merged #28 contract requires one attempt for that shape. Please restore _should_retry() from main, call it from the first-page loop, and retain its truth-table coverage while integrating the pagination tests.
[P2] Keep a public search() contract regression.
The rebase also removes main's top-level successful search() result test and auth-error propagation test; the new suite exercises _walk_pages directly. Because #28 previously lost _run()'s success return while all tests stayed green, please retain at least one fake-page-driven _run() / public search() test that asserts items, rank, query, pagination metadata, and error propagation.
Non-blocking cleanup: tests/test_search.py contains unused _run_walk / _get_walk helpers, and several permanent comments refer to historical “review P1/P2” labels instead of describing the mechanism directly.
Once the retry behavior and top-level regression are restored, I will rerun the focused suite and the same real multi-page CLI matrix; no further browser-logic blocker is currently known.
…op-level search() regression Address the final review round on 2eb912f: - Restore _should_retry() from merged main: the first-page retry now runs only for items=[] together with requiresAuth=true, matching the fancyboi999#28 contract. Unknown zero-item payloads with requiresAuth=false get exactly one navigation instead of two before raising. - Restore truth-table coverage for the predicate, including the requiresAuth=false zero-item shape that must not retry. - Add fake-page-driven public search() regressions: a two-page walk asserting items/rank/item_id/query/pages_fetched/pages_total/ stopped_reason assembly, a default-argument single-page contract test, and error propagation for both the auth wall and the unknown DOM-structure failure - the assembly layer that previously lost _run()'s success return while the suite stayed green is now pinned. - Remove the unused _run_walk/_get_walk helpers and reword the historical review-label comments to describe the mechanisms.
fancyboi999
left a comment
There was a problem hiding this comment.
Approved at 26dc074 after final incremental review and real CLI verification.
The remaining integration regression is resolved: _should_retry() matches merged #28, unknown zero-item responses without requiresAuth are not retried, and public search() success/default/error regression coverage is restored. The cookie records implementation from #28 is retained.
Verification at this exact head:
- Locked Python 3.11 environment:
215 passedwith-W error, 0 skipped/warnings. - Ruff, npm tests
3/3, package dry-run, and diff check: passed. - All five GitHub checks passed, including Python 3.11/3.12 tests and build.
- Real CLI with isolated credentials from the authenticated ego-lite session: MTop
auth statusreturnedvalid=true. X570 --pages 3 --limit 200: 90 items, 90 unique IDs, 3/50 pages, sequential ranks,pages_reached.X570 --pages 2 --limit 45: exactly 45 unique items, 2/50 pages,limit.X570 Taichi --pages 5 --limit 200: 27 unique items, 1/1 page,last_page.- Partial-result failures, invalid-ID handling, pagination shape guards, and retry boundaries are covered by deterministic tests; real service failures were not deliberately induced.
Both Standards and Spec incremental reviews found no remaining actionable issues. No unresolved review threads remain.
Closes #29
Depends on #28 (stacked: this branch is based on
fix/browser-cookie-dual-domain)What
goofish search itemsgains--pages N(default1, fully backward compatible) to walk the search pagination control and accumulate results across pages:goofish search items "X570" --pages 5 --limit 200Why the old approach couldn't page
Probed the live search page (2026-08, Playwright + system Chrome):
scrollHeightfrozen) — the existingauto_scrollis a no-op past the first screen.X570) render a real pagination control:search-pagination-containerwith page boxes1 2 3 … 50, 30 cards per page.?q=X570. Measured page1/2/3 had zero card overlap.?page=2/?pageNumber=2are ignored by the server (still return page 1).So the only working pagination path is clicking the arrow, which the old implementation never did —
limit 50could only ever cover ~1/50 of the results.Implementation
--pages N: click right arrow → wait for SPA re-render by watching the first card id change (8s cap,waitForFunction-style predicate) + 2s settle → extract → dedupe byitem_id→ accumulate.--pagesreached · right arrowdisabled(last page) ·limitreached · a page renders zero new cards (defensive against failed re-renders).limitis repurposed as the cross-page total cap (MAX_LIMIT50 → 200); default 20 unchanged, sopages=1behaves exactly as before.pages_fetched/pages_totalmetadata.Tests
_normalize_pagesclamping,MAX_PAGESconstant (190 passed total),ruffclean.Real end-to-end verification
"X570" --pages 3 --limit 200pages_fetched=3,pages_total=50, 11s"X570 Taichi" --limit 50(single-page query)pages_fetched=1,pages_total=1— stops at disabled arrow"X570" --pages 2 --limit 45Bonus:
pages_total=50metadata gives callers the full result volume before deciding how deep to walk (useful for the price-research workflow that surfaced this gap).