661 Commits
Author SHA1 Message Date
Jakub Orchowski 98e1668721 Add sponsor badge to README
Added a sponsor badge to the README.
2026-09-18 21:26:26 +02:00
ThePhaseless beb281267a feat(github): add GitHub Sponsors funding configuration 2026-09-18 21:06:05 +02:00
renovate[bot] b46614fb55 fix(deps): update dependency playwright to ==1.63.* (#413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-16 02:11:38 +00:00
renovate[bot] cdfd73785a chore(deps): update dependency httpx2 to ==2.13.* (#412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-14 21:29:44 +00:00
renovate[bot] 080b0f9141 chore(deps): update dependency httpx2 to ==2.12.* (#406)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-27 05:27:30 +00:00
renovate[bot] c611afb865 fix(deps): update dependency playwright to ==1.62.* (#369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-27 01:34:31 +00:00
Jakub Orchowski cb2a862386 Merge pull request #400 from ThePhaseless/fix/tls-handshake-398
fix: match a real browser's TLS and solve Cloudflare challenges again
v3.0.4
2026-08-18 12:45:53 +02:00
ThePhaseless 2852dc2527 fix: report an unreachable target as a gateway failure, not a crash
A host that will not resolve arrived as a plain PlaywrightError, which
nothing caught, so Byparr answered 500 and read as its own defect. It is
the upstream that failed, and the failure is usually transient, so 502
also lets clients retry where a 4xx would tell them not to. The timeout
branch stays ahead of it, since PlaywrightTimeoutError subclasses Error.
2026-08-18 12:29:39 +02:00
ThePhaseless 12ef77177a fix: stop waiting for the challenge widget to hold still
scroll_into_view_if_needed waits for actionability, and the Cloudflare
widget animates without pause, so the call timed out at every ancestor
depth and aborted the measurement before the box was ever read. The
container is already in the viewport; drop the scroll and read the box.
2026-08-18 12:02:20 +02:00
ThePhaselessandClaude Opus 5 11d7e59263 test: assert what the challenge tests claim to assert
Three tests checked solution.status against HTTPStatus.OK, which the
endpoint now hardcodes, so the check could never fail. Drop it everywhere
except the test that exists to pin that behaviour.

That left the self-clearing case asserting only that nothing raised, which
the click test already covers. Give it a measurable widget and assert we
never press it: a challenge that clears on its own is the one case where
touching the checkbox would be wrong, and nothing tested it. Verified by
moving the click ahead of the exit check, which the assertion now catches.

Drop the fake page.evaluate return as well, since /v1 stopped calling it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 04:00:17 +02:00
ThePhaselessandClaude Opus 5 78b3f314c3 refactor: report 200 like FlareSolverr and split the challenge out
FlareSolverr hardcodes the solution status ("todo: fix, selenium not
provides this info"), so clients built against it never see anything else.
Byparr handed back the real navigation code on the branch without a
challenge and 200 on the branch with one, which is neither honest nor
compatible. Always report 200.

Move the challenge handling into src/challenge.py and the response bodies
into src/content.py, leaving endpoints.py with the routes and navigation.
That also confines the import of playwright_captcha's private detection
module to a single file, so a patch release can only break one import
instead of the app and the test module at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 03:51:34 +02:00
ThePhaselessandClaude Opus 5 d0b013029c fix: load the page the challenge was hiding, and stop trusting a stray box
Nothing waited for the destination once the interstitial let go: page_html
stayed unset and the networkidle wait only ran on the branch that never saw
a challenge, so a challenge clearing on its own returned whatever had loaded
by then. Wait for it on both branches.

An exhausted budget produced timeout=0, which Playwright reads as no timeout
at all, turning every remaining wait unbounded exactly when it should fail
fast. Floor it instead.

Scroll the widget into view before measuring it, since bounding_box reports
viewport coordinates and an off-screen widget was clicked at a point that hit
nothing, and reject containers taller than a checkbox row so a full-page
wrapper cannot pass for one - both reported success while clicking blank
space.

Drop max_attempts, which nothing reads now that the solver is gone, and
refresh AGENTS.md, which still described camoufox and a solver in the
dependency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 03:41:58 +02:00
ThePhaselessandClaude Opus 5 426aae4310 fix: address review findings on the challenge solver
nowsecure.nl carries no interstitial, so the turnstile detector only widened
the entry condition without ever being able to satisfy the exit one, leaving
a turnstile-only page reported as solved on the first poll. Detect on the
interstitial alone.

Restore the user-agent guard that went missing: an absent header made
Solution reject None and turned into an unhandled 500, where it used to
degrade to an empty string. Fall back to "" directly rather than reaching
for evaluate(), which CDP refuses.

Bound the widget measurement, which inherited Playwright's 30s default at
each of four depths and so could run far past the request budget, throttle
the probe when no click lands so the scan no longer repeats every tick, and
release the mouse button through a finally so a failed press cannot leave it
held down.

Cover the click path: the fixture pinned bounding_box to None, so nothing
exercised the code this branch exists to add.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 03:30:49 +02:00
ThePhaselessandClaude Opus 5 3dcf529609 refactor: drop the solver and prove the challenge page is gone
Nothing calls ClickSolver any more now that the checkbox is clicked through
the mouse, so take it out of the browser dependency instead of leaving it
constructed but unused.

Assert on the returned body as well: a 200 alone passed even when the
interstitial itself was handed back, which is the defect this branch fixes.
Judge on _cf_chl_opt, since Cloudflare keeps serving cdn-cgi/challenge-platform
as a beacon on pages that are already cleared.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 02:45:19 +02:00
ThePhaselessandClaude Opus 5 61db8d82ee fix: click the challenge checkbox without tripping Cloudflare's detector
Measuring the widget with page.evaluate() made Cloudflare reissue the
challenge every few seconds, so the interstitial never cleared. Locate the
container with locators instead and click it through the mouse, which leaves
its closed shadow root untouched.

Keep the solver lazy as well: ClickSolver.prepare() patches attachShadow,
which is what escalated a self-clearing challenge into a checkbox in the
first place.

A click can land while the widget is still self-verifying, so retry on a
cooldown for as long as the request budget lasts rather than stopping after
the first one, and confirm the marker is gone twice before reporting success
since it drops out between challenge rounds.

The bypass tests now pass on their own, so drop the xfail marks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 17:45:15 +02:00
ThePhaselessandClaude Opus 5 9067ca37df test: simplify the xfail reason
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TDMac4vGGcBhoUB5V6bvFK
2026-08-17 12:38:14 +02:00
ThePhaselessandClaude Opus 5 cb8fb58fa8 refactor: use the library's detector instead of hand-rolled selectors
detect_cloudflare_challenge(page, 'interstitial') matches the challenge on
the page and stops matching once it clears, measured on ext.to (True then
False), and on yggtorrent, nowsecure.nl and google.com, none of which carry
a /cdn-cgi/challenge-platform/ script when cleared. So the custom marker set
was unnecessary.

The turnstile variant is not usable for this: nowsecure.nl embeds turnstile
scripts on its normal page, so it reports a challenge even when cleared.

Also adds PlaywrightTimeoutError to the retryable set. It is a different
class from the builtin TimeoutError -- playwright's derives from its own
Error -- so a Playwright timeout inside the solver escaped the loop and
returned 408 without retrying.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TDMac4vGGcBhoUB5V6bvFK
2026-08-17 11:35:49 +02:00
ThePhaselessandClaude Opus 5 fc64fe05d5 refactor: drop what is not part of the fix
Replaces the try/except-plus-log around the solve attempt with
contextlib.suppress, and inlines the one-use attempt cap. Type annotations
are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TDMac4vGGcBhoUB5V6bvFK
2026-08-17 11:30:29 +02:00
ThePhaselessandClaude Opus 5 24c339b085 fix: judge the challenge by its markup, not by networkidle
solve_captcha decides it failed by waiting for networkidle, which returns
while Cloudflare is still verifying, so it reported failure on challenges
that had already passed and retried until the whole max_timeout burned into
a 408. Cap each solve attempt and let the challenge markup decide instead.

CHALLENGE_MARKERS has to match the orchestrator specifically: the bare
/cdn-cgi/challenge-platform/ path also matches the jsd beacon Cloudflare
serves on cleared pages, so detect_cloudflare_challenge never reports
success on its own.

Also switches the solver back to FrameworkType.PLAYWRIGHT, since PATCHRIGHT
injects the shadow-root unlock over CDP and Firefox has no CDP session, and
turns off COOP/COEP so the widget's iframe appears in page.frames at all.

The four sites Cloudflare refuses on this browser are xfail rather than
skip, so a regression still shows and a pass records as xpass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TDMac4vGGcBhoUB5V6bvFK
2026-08-17 11:21:57 +02:00
ThePhaselessandClaude Opus 5 c7633b4525 revert: leave max_attempts alone
sys.maxsize was the original bug, but only because solve_captcha was called
in a retry loop. That call is gone, so MAX_ATTEMPTS now only reaches
ClickSolver at construction and never bounds anything; the solve loop is
bounded by timer.remaining(). Reverting a change that no longer does
anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TDMac4vGGcBhoUB5V6bvFK
2026-08-17 11:13:40 +02:00
ThePhaselessandClaude Opus 5 691aaa6f3f refactor: strip the challenge-solver changes to the minimum
Removes every explanatory comment added by this branch, inlines the browser
prefs rather than holding them in a module constant, folds _cloudflare_frame
into its only caller, and cuts the added docstrings to one line each.

No behaviour change: 13 unit tests pass, and removing the checked-box guard
still fails its test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TDMac4vGGcBhoUB5V6bvFK
2026-08-17 11:06:09 +02:00
ThePhaselessandClaude Opus 5 e0b1efa560 style: trim the challenge-solver comments to what is load-bearing
Cuts ~100 lines of commentary that restated the diff or recorded dead
investigation, and merges _press_point back into _press_checkbox now that
the checked guard is one condition rather than the extra return that
tripped the too-many-returns lint.

Corrects the COOP/COEP note, which claimed the pair changed no outcome.
Without those prefs the widget's iframe never appears in page.frames at
all: measured on ext.to, eight presses land with them and none without.

No behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TDMac4vGGcBhoUB5V6bvFK
2026-08-16 21:16:20 +02:00
ThePhaselessandClaude Opus 5 362b07e55a docs: correct the xfail reason with what was actually measured
The reason said Cloudflare refuses the checkbox click from datacenter IPs
and that the visitor's address was being judged rather than our code. That
is false. Measured from one datacenter IP within a single hour, byparr
v2.1.0 clears ext.to in 18s, speed.cd/login in 20s and extratorrent.st in
19s, each returning cf_clearance, while no configuration of the current
stack clears any of them.

Two candidate causes were measured and eliminated rather than assumed. The
JS-visible fingerprint is not it: camoufox is the less coherent of the two
browsers -- no WebGL at all, oscpu leaking Linux beneath a Windows UA -- and
passes regardless. The TLS handshake is not it either: re-enabling cipher
0xC009 reproduces camoufox's JA4 byte for byte
(t13d1717h2_5b57614c22b0_3cbfd9057e0d) and the challenge is still refused.

Also records that devtools.jsonview.enabled is load-bearing for #394, and
that the COOP/COEP pair changed no outcome on any site or network measured.

No behaviour change; comments and the xfail reason only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TDMac4vGGcBhoUB5V6bvFK
2026-08-16 21:10:08 +02:00
ThePhaselessandClaude Opus 5 1a2cf32e1e fix: press the Cloudflare widget instead of its invisible checkbox
playwright-captcha's ClickSolver clicks the challenge's input element
directly. That input sits under a styled overlay, so Playwright reports a
successful click while `checked` never flips -- which is why the
interactive challenge has never been solved here. The solver also judged
its own click by waiting for networkidle, which returned 9ms later while
Cloudflare was still verifying, so it reported failure on challenges that
were about to pass.

Replace it with a poll loop that watches for the challenge markup to go
away and presses the widget's visible pixels whenever an unchecked box is
on offer. A box that is already checked is left alone: pressing over the
top of Cloudflare's verification restarts it, and ext.to and speed.cd sat
on "performing security verification" for a full 300s budget while being
pressed a dozen times.

Measured on a residential connection, driving the real /v1 handler:
nowsecure.nl passes in 3s, extratorrent.st in 116s and 1337x.to in 198s,
all three returning cf_clearance. extratorrent.st had never cleared
before, on any network or solver. ext.to and speed.cd still refuse -- the
press registers and the widget re-serves a fresh unchecked box -- so they
stay in the xfail list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TDMac4vGGcBhoUB5V6bvFK
2026-08-16 19:11:31 +02:00
ThePhaselessandClaude Opus 5 f97e3d325d fix: restore v2's COOP/COEP handling so the checkbox is reachable
v2.1.0 launched camoufox with disable_coop=True. v3 dropped it, and without it
Cloudflare's challenge iframe -- which carries allow="cross-origin-isolated" --
lands in an isolated content process where Juggler sees no docShell:
content_frame() raises "Permission denied to access property docShell on
cross-origin object" and the solver never reaches the checkbox to click it.

Not a demonstrated win. From a datacenter IP Cloudflare rejects the click
however it is delivered -- measured across eight sites, nine consecutive
clicks, a humanized cursor, four fresh navigations, and a shadow-root patch
made undetectable (no global flag, toString reporting native code). The same
browser and IP clear nowsecure.nl and come back with a cf_clearance cookie, so
what is being judged is the address, not the client.

Restored for parity with the version users report working, because reaching the
checkbox is a precondition for ever passing an interactive challenge and Byparr
mostly runs from residential addresses that Cloudflare treats far better than a
CI runner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 13:45:57 +02:00
ThePhaselessandClaude Opus 5 4e70c8b208 fix: bound the challenge solver and pin the TLS handshake
Follow-up to the earlier CI fix, after A/B-ing every change against main and
against this branch's original commit.

What measurably changed, and what did not:

- The solver's retry loop was unbounded (max_attempts = sys.maxsize). On a
  challenge it cannot clear it retried ~1300 times per request and the caller
  waited out the entire max_timeout for a 408 it was always going to get.
  _solve_challenge now clicks, waits for the challenge markup to actually
  disappear, and gives up when the budget does.

- That wait exists because the solver's own verdict is worthless here: it
  judges its click with wait_for_load_state("networkidle"), which returned 9ms
  after the click while Cloudflare was still showing "verifying you are
  human", and then reported failure.

- The "is it still up?" check cannot use detect_cloudflare_challenge alone.
  That matches any script under /cdn-cgi/challenge-platform/, and Cloudflare
  serves its jsd bot-scoring beacon from the same path on cleared pages. Nor
  can it use the widget iframe: a cleared nowsecure.nl carries two of those
  with no challenge present. CHALLENGE_MARKERS matches the challenge
  orchestrator script and the interstitial's own markup.

- test_tls_handshake_looks_like_firefox pins what this branch is actually for.
  Measured through /v1 on the same host: main offers 52 cipher suites, this
  branch 16, and real Firefox offers 16. route.fetch() was re-issuing
  navigations through Playwright's HTTP client, and that is a fingerprint no
  header spoofing hides. Unlike a Cloudflare verdict the count is
  deterministic, so it is the one assertion here that cannot flake.

- Disabling COOP/COEP does let the solver reach and click the checkbox for the
  first time (Cloudflare advances to "verifying you are human"), but it changed
  no outcome across eight sites, and real Firefox ships those policies on.
  Recorded in a comment rather than shipped.

test_bypass keeps a hard assertion against targets that clear from any network.
The four Cloudflare guards hardest move to xfail rather than skip: they still
run and still report, but Cloudflare's opinion of the runner's IP cannot turn
the build red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 13:27:53 +02:00
ThePhaselessandClaude Opus 5 5130400571 fix: stop an unreachable Cloudflare widget from burning the whole timeout
The bypass tests were failing on CI with 408s after 78 minutes. Neither the
runner's speed nor this branch's TLS change was responsible.

On the sites that fail, Cloudflare serves its interactive checkbox challenge.
playwright-captcha locates the widget iframe inside the shadow root and then
calls ElementHandle.content_frame(), which this Firefox build refuses:

  Protocol error (Page.describeNode): Permission denied to access property
  "docShell" on cross-origin object

Its fallback -- matching page.frames by URL -- cannot help either, because the
challenge frame exposes an empty URL to the parent. Every attempt therefore
ends in CaptchaDetectionError: Cloudflare iframes not found.

MAX_ATTEMPTS was sys.maxsize, so that repeated until the request budget ran
out: 432 docShell errors and 1326 retry iterations in a single request on the
runner, and with max_timeout raised to 360 and --retries 3, a 1h18m job.

Three changes:

- max_attempts defaults to 5. An unreachable widget stays unreachable, so the
  retries were not buying anything; the caller now hears about it in seconds.
- _solve_challenge translates the solver's own give-up exceptions into the 408
  read_item already reports for timeouts. Without this, bounding max_attempts
  would have turned the hang into an unhandled 500.
- The solver framework goes back to PLAYWRIGHT. PATCHRIGHT skips the
  unlockShadowRoot init script and injects over CDP instead, which Firefox has
  no session for ("CDP session is only available in Chromium"). Cloudflare
  builds its widget in a closed shadow root, so on this branch the challenge
  iframe was invisible even to page.locator: 1 -> 0 against the same sites on
  the same runner.

test_bypass drops the max_timeout=360 override and skips again on 408.
Whether Cloudflare shows the interactive challenge depends on the visitor, so
the runner's luck should not decide whether a regression of ours is reported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 11:09:30 +02:00
ThePhaseless 14834c23b2 increase test timeout 2026-08-15 00:41:34 +02:00
ThePhaseless aa9a331064 reformat, upgrade and fix timeout 2026-08-15 00:15:50 +02:00
ThePhaseless c4dcee3e2b log test skip reasons/websites 2026-08-14 23:01:25 +02:00
ThePhaselessandClaude Opus 5 652c234782 refactor: drop redundant navigator.userAgent evaluate
page.goto() returns None only for about:blank or a same-URL-different-hash
navigation, so page_request is always present for a real request and its
headers always carry the UA. The evaluate call was therefore unreachable
as a fallback and, once moved before navigation, silently became the
primary source instead.

Request headers are also the correct source: consumers replay them with
the clearance cookies, so the UA the server saw is the one to report.
This restores the ordering d3a828e established.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 22:22:53 +02:00
ThePhaselessandClaude Opus 5 1c2b2df90d style: collapse setup_routes docstring to one line
Fixes the ruff D200 the docstring edit introduced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 22:09:01 +02:00
Alex Thomson c9d4cd4a4e fix: owui tests 2026-08-14 21:36:41 +02:00
Alex Thomson ecf7c03d7c fix: remove CSP stripping 2026-08-14 21:36:41 +02:00
Alex Thomson c96db89d03 fix: call evaluate before navigation
Avoids any Content Security Policies that maybe present after navigation
2026-08-14 21:36:41 +02:00
Alex Thomson 742cafc64a refactor: replace evaluate with locator 2026-08-14 21:36:41 +02:00
Jakub Orchowski 9afb3e0903 Merge pull request #395 from ThePhaseless/fix/csp-json-viewer-eval
fix: /v1 must survive CSP-blocked evaluate (incl. Firefox JSON viewer)
v3.0.3
2026-08-11 11:57:47 +02:00
ThePhaseless d7792b8fcd fix(test): assert camelCase userAgent key in JSON response 2026-08-11 11:40:49 +02:00
ThePhaseless bb526d73b0 merge: resolve conflict with main (read_item refactor #393) 2026-08-11 11:30:23 +02:00
ThePhaseless 9b933ea70c chore: drop explanatory comments 2026-08-11 11:23:31 +02:00
ThePhaseless d3a828e814 fix(v1): source User-Agent from request headers; evaluate only as fallback
page.evaluate runs eval() in the page's main world, which fails with 'call to eval() blocked by CSP' under any CSP that disallows unsafe-eval - HTTP headers (already stripped), meta tags (not strippable), or internal viewer documents (#394).

The navigation request already carries the UA the site actually saw, so take user_agent from page_request.request.headers and keep evaluate only as a best-effort fallback whose failure can no longer 500 the request.
2026-08-11 11:14:10 +02:00
ThePhaseless 46a3c68eb0 fix: disable Firefox JSON viewer so evaluate works on JSON APIs
Firefox renders application/json documents in a built-in viewer whose own
CSP (<script-src resource:>) blocks Playwright's eval-based page.evaluate,
crashing /v1 with a 500 on JSON APIs (closes #394). Setting
devtools.jsonview.enabled=false renders JSON as plain text, which also
returns the raw JSON body instead of the viewer's syntax-highlighted HTML.
2026-08-11 11:08:04 +02:00
Jakub Orchowski f821290bac Merge pull request #393 from ThePhaseless/chore/ruff-lint-cleanup
chore: fix ruff lint findings and refactor read_item
v3.0.2
2026-08-11 00:33:56 +02:00
Jakub Orchowski 5de9266d7e Merge pull request #392 from ThePhaseless/fix/fake-dep-locator-mock
test(fake_dep): mock Playwright locator API faithfully
2026-08-11 00:21:21 +02:00
ThePhaseless 89dcf5e16c Merge branch 'main' into chore/ruff-lint-cleanup
Resolved conflicts in src/consts.py and src/endpoints.py:
- consts.py: take theirs (CHALLENGE_TITLES removed, browser_locale added,
  CaptchaType import no longer needed — detection is now library-based)
- endpoints.py: merge both refactors — keep theirs' detect_cloudflare_challenge
  + page_html capture, reapply my helper extraction (setup_routes,
  _navigate_and_solve, _solve_challenge, _wait_for_networkidle,
  build_response_content, _fetch_pdf_content) on top
2026-08-11 00:20:55 +02:00
ThePhaseless 3c45ef9691 chore: fix ruff lint findings and refactor read_item
- Fix I001: sort imports in src/consts.py
- Fix PLC0415: move `import base64` to top of tests/main_test.py
- Fix UP037: remove quotes from LinkResponse return annotation
- Fix D213: correct multi-line docstring summary placement
- Remove unused `# noqa: BLE001` in src/owui.py
- Refactor read_item into helpers: setup_routes, load_page_and_solve,
  build_response_content, _fetch_pdf_content — resolves C901 and PLR0915
- Add CPY001, BLE001 to ruff ignore list
2026-08-11 00:15:39 +02:00
ThePhaseless bd4c62de38 test(fake_dep): drop explanatory comments 2026-08-11 00:01:29 +02:00
ThePhaseless 92043725f5 test(fake_dep): mock Playwright locator API faithfully
fake_dep's AsyncMock page made page.locator() return an un-awaited
coroutine, so detect_cloudflare_challenge swallowed an AttributeError
and reported a challenge. The networkidle-timeout test silently ran the
solver branch and never exercised its intended path, plus emitted a
'coroutine ... was never awaited' RuntimeWarning in CI.

Make page.locator() sync-returning (as in real Playwright) with an
awaitable count() that finds no elements, and assert the solver is never
invoked.
2026-08-11 00:00:10 +02:00
Jakub Orchowski f8d087bab8 Merge pull request #391 from ThePhaseless/fix/tmpfs-python-wipe
fix: keep uv Python out of tmpfs-mounted /tmp
2026-08-10 23:46:59 +02:00
ThePhaseless c38a6f4e85 fix(docker): keep uv Python out of tmpfs-mounted /tmp
HOME=/tmp put the uv-managed Python at /tmp/.local/share/uv, so a
tmpfs mount on /tmp (e.g. compose tmpfs: /tmp) wiped the interpreter at
container start, leaving the /app/.venv/bin/python symlink dangling and
startup failing with 'exec /app/.venv/bin/python failed: No such file
or directory' (#389).

Move HOME to /home/byparr and apply the OpenShift permission pattern
(owner uid 1000, group 0, group=user) so both the default user and
arbitrary-UID runtimes (docker run --user, OpenShift) can write to it.
Apply the same pattern to /cache, where invisible_playwright keeps
runtime browser/profile data and which arbitrary UIDs previously could
not write.

Fixes #389
2026-08-10 23:40:16 +02:00