get_final_path_from_real_time normalises elapsed time onto the sigmoid's
input range with a 4.5 multiplier, and logistic_sigmoid(4.5) is 0.978, so
the bezier was never evaluated at its endpoint. move_mouse then looped on
`while time.monotonic() < end_time`, so it never asked for a sample at
movement_time either and the `t > movement_time` branch was unreachable
from there. The pointer was left short of the point move_to_element chose,
on every move.
Clamping the loop's last sample to movement_time is the smaller half of
this, but on its own it only recovers 0.05px of a 2.8px median offset,
since the sigmoid is what the rest of the shortfall comes from. So the
path now hands back the endpoint once t reaches movement_time rather than
only once it passes it, and the clamp is what makes that branch reachable.
Measured over 6000 simulated moves at 200px travel, against a 10x10
control: median offset 2.82px and 13.02% of clicks landing outside the
element before, 0.00px and 0.00% after.
Also comments the 4.5 where it is, since the two are only correct
together.
The cleanup after each task called close_all_other_tabs() with no arguments.
That defaults `exceptions` to the currently focused handle, so when a task died
with a Bing tab in focus the cleanup kept the Bing tab and closed the Rewards
one. Every later task then looked for Rewards controls on a search results page
and reported [SKIP] for a UI that was present and working the whole time. One
failure cost the rest of the run.
Measured in the container rather than reasoned about: with a Rewards tab and a
Bing tab open and the Bing one focused, close_all_other_tabs() leaves
['https://www.bing.com/search?q=weather'] behind and the Rewards handle is gone.
Two separate things have to hold before the next task can work, and they failed
independently. The right tab has to survive, so restore_main_tab names the
handle recorded at startup instead of trusting whatever happens to be focused.
The surviving tab also has to be showing the right page, which the tab fix alone
does not give you: run_search_batch navigates the main tab to bing.com itself,
so a failure part way through that batch strands the main tab on a search page
with no second tab involved at all.
Navigating only happens when the step did not finish, and is skipped when the
browser is already on the Rewards host, so a clean run navigates nowhere and a
panel that merely failed to render does not cost a page load.
Both recoveries swallow their own errors. Raising here would replace the real
failure with the tidy-up's, which is why the close_all_other_tabs call was
wrapped in the first place.
End to end in a real browser: two tabs with the Bing one focused become one tab,
the original handle, on rewards.bing.com/about (the logged out redirect), and a
main tab stranded on bing.com recovers too. Fifteen unit tests cover the tab
choice, the missing-main-tab fallback, the failure and skip paths, a clean run
navigating nowhere, and a driver too broken to do either.
Refs #63
Claude-Session: https://claude.ai/code/session_0113hp36oUBeMqG7twfuC8rE
Every failed task printed the same reason, because the blanket catch in
complete_all_tasks treated NoSuchElementException and TimeoutException as one
finding. A wait that expires says nothing about whether the section was there,
since wait_for_element swallowed whatever the getter raised and returned False.
A market that does not ship visual search and a points panel that was on screen
and slow both arrived as TimeoutException
wait_for_element now keeps the last reason the getter gave and raises
ElementNeverAppeared when nothing was in the DOM for the whole wait.
_container_by_id raises ElementNotReady when the id is present but no visible
copy has content yet, which is the hydrating case it used to report as missing.
Both subclass what they refine, so claim_bonus_points and
complete_bing_daily_set keep working untouched
A task that was reached and then ran out of time is now a [FAIL] rather than a
[SKIP], because it may have left points behind. Absence stays a [SKIP]
56 tests pass, 20 of them new. A mutation check backs them, forcing every
expired wait to count as absence fails 2, and collapsing the two branches back
into one [SKIP] fails 4
Refs #52
Matching on bing.com/search alone was too narrow. Turn referrals into rewards is a real daily set activity that awards points and it points at a rewards url, so the filter skipped it and the run came up short without saying why.
Three shapes now count, bing.com/search, bing.com/rewards and rewards.bing.com. The bing app promo from #45 sits on bingapp.microsoft.com and stays out of all three, which is what keeps that fix intact. Five tests, one per shape plus a mixed panel.
Upstream widened the except in close_all_tabs_except to catch
NoSuchWindowException; this branch had rewritten the same two lines from print
to logging. Both are wanted, so the resolution keeps the wider except and the
logger call, and takes the new import.
Claude-Session: https://claude.ai/code/session_01RCncv1HC4UvoJcYqBtwxrt
The Docker section still said the Linux case was untested. It is not: the
two-account run was done that way, with a profile signed in on a Linux host
opening in the container already on the dashboard. Only macOS is still
unmeasured, and it is expected to fail the way Windows does.
Also say what a killed browser leaves behind. A profile whose browser was
killed keeps a SingletonLock naming the machine that wrote it, and the
container reads that as the profile being open elsewhere, which produces the
same startup error a genuinely open window does and is not obvious from it.
Claude-Session: https://claude.ai/code/session_01LVjQemhtfybkHjJFxqMu19
#50 landed a tests directory using stdlib unittest, discovered with
python -m unittest discover -s tests. The multi-account checks were a
standalone script with their own runner, so discovery would have walked
straight past them.
Same cases, rewritten as TestCases. The layer that starts Edge twice is
behind REWARDS_BROWSER_TESTS rather than a --browser argument, since
discovery does not pass arguments through.
Claude-Session: https://claude.ai/code/session_019PWUtibJn81iDbxZ4hzeU3
Five layers, cheapest first: which accounts a configuration produces, the
flags each one hands Edge, the run loop's ordering and exit codes, one
account failing every way it can without ending the batch, and two real
Edge profiles holding two independent, persistent identities.
Layers 1 to 4 need nothing installed beyond selenium and run in a second.
Layer 5 starts Edge twice and reaches bing.com, so it is opt in behind
--browser. It keys on bing.com's own MUID rather than an injected cookie:
a cookie added through webdriver is not written to the profile the way a
Set-Cookie is, so it would prove nothing about a sign-in surviving.
Claude-Session: https://claude.ai/code/session_019PWUtibJn81iDbxZ4hzeU3
The positional fallback returned whatever sat at index 3 of the streaks section. On a partially rendered page that is not the daily set: observed live returning 'Mobile App | Check-in: 0/1', and clicking it opens the app store page instead of the panel, which is what #45 and #46 describe. It now checks the label before handing the button back and raises otherwise, so the task is skipped rather than the wrong streak clicked.
read_search_points waited for the close button before reading anything, so a panel that rendered its content but not its button failed the whole search task while the number was already on screen. Traced to that wait with a stacktrace. Closing is best effort now.
The panel hydrates progressively, so waiting for the section only tells you it opened, not that it filled. A check running on the first non-empty state reports whatever happened to be rendered at that moment, which is why the daily set came out differently run to run. Same wait as complete_bing_daily_set: hold out for the full set, report what is there if it never arrives.
Win32 strips a trailing dot off a path component and python's normalisation
does not, so such a name is not the directory it reads as. REWARDS_ACCOUNTS=...
resolved to data-dir itself, which is the default profile the resolved-path
check was added to keep named accounts out of, and personal,personal. passed
the duplicate check as two entries while sharing one profile on disk. Both
break the one-directory-per-account guarantee this module exists for.
Reject the shape by name, and resolve with realpath rather than abspath so a
link or a junction under data-dir is followed to where it really goes.
Only SessionNotCreatedException was isolated. Every other way an account can
fail reached main() and took the accounts after it with them: a driver that
will not start for another reason, an unwritable profile directory, the first
page not loading, the browser dying mid-run, or quit() raising because it was
already gone. With REWARDS_ACCOUNTS=one,two,three and the middle one failing,
three never ran and main() exited on a traceback instead of an exit code.
Catch it around run_account, report it the way a failed task is reported, and
carry on. KeyboardInterrupt is left alone so Ctrl-C still stops the run. The
quit() in run_account is guarded too, so a tidy-up that raises no longer hides
the failure it was tidying up after.
Four bugs that reached real runs are covered: a point value the parser could not read, the wrong row of the breakdown panel, a container that looks right but is empty, and a label that matches two different buttons.
Stdlib unittest with small fakes for the selenium calls the selectors make, so this adds no dependency and needs no browser. Run with python -m unittest discover -s tests.
get_daily_set_elements returned everything after the first link in the panel. The panel also carries promotional links, so position hands one back as an activity. Clicking it leaves rewards.bing.com and every element captured before that goes stale, which is the exception reported in #45.
Activities always point at a Bing search, so match on that. When nothing matches it returns nothing rather than falling back to position, since clicking a promo is worse than skipping the task and complete_bing_daily_set already reports the shortfall.
The character check allowed "." and "..", which are made entirely of allowed
characters and still walk out of the directory, so the traversal guard only
stopped the cases containing a separator. Reject both by name and check the
resolved path against the profile root as well, since the character set
constrains the characters rather than where they point.
Also drop the MouseUtils and KeyboardUtils built in run_account and never
used, RewardsTaskUtils builds its own, along with the two imports that
leaves unused, and say "lowercased" in the no-source warning, which is what
the code returns.
visual_search.jpg is gitignored and excluded by .dockerignore, so the
container had no file at the path rewards_tasks.py uploads and the visual
search task was the one task that could not run in it. Bind mount it from
the project root, where src/random_image_for_visual_search.py writes it.
Also add the blank line the Logging heading needs to render.