Fix the tombstone flow, and watch what Cyd actually sends - #717
Merged
Merged
Conversation
X opens /compose/post as a modal over the home timeline, and that timeline has an inline composer carrying the same test identifier. Playwright re-resolves .first() on every poll, so the moment the modal closed the wait retargeted to the inline composer, which stays attached forever. The wait could therefore never succeed. Every post that actually landed was reported as a failure and retried up to three times, which on a 41-post seed means triple-posting and a daily cap spent on duplicates. Seen for real: thread-001 posted, was called a failure, and went back for another try. Scoping the wait to the dialog's own field is what says the post landed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The long-form shape needed a premium account and X's own composer, so the seeder could only ever hand it to a person to do by hand. There is no premium account and there is not going to be one, which made it a step that was always skipped and a shape that was never seeded. So it goes, rather than staying as a permanently unreachable branch: the kind, the manual action, the shape marker, the switch case, both assertions, and the README passages that promised it. The walk checklist and the indexing test's note go with it, since neither describes anything that can happen now. Long-form now has no representation anywhere — no seeded post, no fixture, no marker. That is a risk accepted knowingly: it was already the case that no fixture could vouch for the shape, and inventing one would assert what the capture never saw. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…h to find it The seeder counted how many likes it had made and reached for that index in the timeline. Both halves of that work against how X behaves. X virtualizes the timeline: posts scrolled well past are dropped from the DOM, so the number rendered stays roughly flat however far down the feed you get. A liked post also leaves the `like` selector entirely, its test identifier flipping to `unlike`. So the index climbed while the count did not, and past eight or so actions it could never be reached. The first match is always the next one to act on, for the reason that broke the old approach: what has been done is no longer in the set. The per-kind counters existed only to feed that index, so they go too. That alone was not enough. Every fifteen actions the seeder reloads the feed, which puts it back above everything it has already acted on, so the scroll needed to reach the next one grows with the count. On an image-heavy feed a single photo post runs to most of a screen, and thirty already-bookmarked posts sat further down than eight turns of the wheel could reach. Both failures looked identical to a selector X had changed, which is the reason to fix them rather than record them: they were filling the findings log with evidence of our own bugs. Measured on one run of @nexamind91326: 4 failures in 41 likes before the first change, one skipped bookmark after it, and none across bookmarks 31-41 and twenty-five follows after the second. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The finished page has a block per kind of run — save, archive, delete, and both Bluesky migrations — each keyed off the jobs type stored when the run started. The tombstone wizard stores "tombstone", and no block matched it, so the page rendered its "Here's what I did" heading above nothing at all. The one run whose whole purpose is changing how your profile looks to other people was the one run that would not say what it had changed. It lists what was actually turned on, the way the delete block does with unfollowing: the banner, the bio, and the lock are each shown only if that part of the tombstone was enabled. There are no counters to report here, so the account settings are the only thing to read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tombstone page pre-fills its bio field from the bio Cyd has saved, which is deliberate: it offers you what is on your profile now, rather than whatever tombstone text you last typed. But that saved copy is written during login and nowhere else, so the job that changes the bio left it holding the old value. Come back to the page after tombstoning and it offered the bio the tombstone had just replaced. The job already reads the bio back off X to confirm the save landed, so the value worth keeping is right there and is known to be what X holds. It gets written to the account only on that path, so a bio that did not change leaves the saved copy alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The banner job waited for the crop step's Apply button as though X always showed one. It does not. Seen today on @nexamind91326: the banner went straight into the profile dialog, ready to save, and the job spent its full timeout waiting for an Apply button that was never coming — then filed an automated error report about a run that was otherwise about to succeed. The probe on 2026-09-15 saw the crop three times out of three, which is exactly why waiting for it looked safe. Three observations of a thing X does sometimes is not evidence that X does it always. So the crop is taken when offered and skipped when not, on a shorter timeout so a run that will not get one is not held up. Nothing is lost by not insisting: the banner is read back off the profile afterwards either way, and that read is what says the save landed. Which steps X put in the way is not something worth failing over. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reading the banner back is what says a save landed, and the read was happening before X had drawn anything. The page load finishes, the header photo arrives after it, and asking at that moment matches no element and comes back "". Both reads did that, before and after. Two empty strings match, so the job announced that the banner had not changed — on a save that had gone through and could be watched arriving in the webview. The check meant to catch a silent no-op was manufacturing one. Seen on @nexamind91326 with bannerBefore and bannerAfter both "", against a webview still showing its loading spinner. So the read waits for the banner to exist before asking for its URL. A profile with no banner never draws one, and that wait running out is an answer — there is no banner — rather than something to fail over. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The capture tooling answers questions by driving a separate Chromium, so its findings are about Chromium. That is fine until the app and the probe disagree, which is where this stands today: a tombstone banner that uploads under Playwright and does not under Electron, with nothing recorded on the Electron side to compare. The session already has a listener on every request, reading operation identifiers and rate limits off them. It now also records the method, the URL and the status, when CYD_REQUEST_LOG names a directory to write to: CYD_REQUEST_LOG=/tmp/cyd-requests npm start Which answers, for a run that has already failed, whether a call was made at all and what came back. For the banner that is the whole question: no i/media/upload means Cyd never uploads, and an upload that returns an error means X refused it. Those need different fixes and nothing so far tells them apart. Off unless asked for, and it holds no bodies, no headers and no cookies. What it records is the traffic of a signed-in session, so it stays on the machine: it is never attached to an error report. A line at a time, because the runs worth reading back are the ones that did not reach the end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Recorded from Cyd's own session, on a run that worked: 19:55:53.443 POST 200 update_profile_banner.json 19:55:55.962 GET 200 profile_banners/.../1789501624 19:55:56.163 GET 200 profile_banners/.../1789502153 X took the banner. Then the profile page drew the banner it already had, and replaced it two hundred milliseconds later. The read-back asked once, in that gap, and got the old URL — which matches the one read before the save, so the job announced that the banner had not changed and filed an error report about a save that had gone through. That is the whole of it. Waiting for the image element to exist did not help: it exists immediately, holding the old source. It is a race, which is why this failed four times and then passed with nothing altered in between, and why every explanation that fit the failures had to also explain the success. So the banner is read until it differs from the one before, rather than sampled once. A banner that never turns over still reports unchanged, which is the case this check was built for. Found with CYD_REQUEST_LOG, which existed for about four minutes before it paid for itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Recorded from Cyd's own session, on a run that failed: 20:00:17.681 POST 202 i/media/upload.json?command=INIT 20:00:18.005 POST 204 i/media/upload.json?command=APPEND 20:00:18.471 POST 201 i/media/upload.json?command=FINALIZE 20:00:25.671 POST 200 account/update_profile.json The image uploaded. Then Save wrote the name and the bio, and update_profile_banner.json was never called at all. Next to a run that worked, that call is the only difference. So Apply is not a step X sometimes puts in the way, which is what the last change assumed. It is what stages the uploaded image as the banner, and a delivery that draws no crop has staged nothing. Carrying on to Save from there uploads a file, attaches it to nothing, and comes back reporting a banner that did not change — which describes none of what happened, and is what four error reports today were actually made of. The banner now goes on again, up to three times, until X offers a crop. If it never does, that is the error, said plainly, rather than a save that cannot work followed by a read-back left to describe the wreckage. Takes out the test that asserted a save without a crop succeeds. It passed, and it was wrong: nothing had watched what X was really sent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 #712.
Work from the manual verification in #712. The X automation was already
repaired; everything here was found by running it by hand against a seeded
test account, which is what that ticket exists to do.
Tombstone
Five bugs, none in the automation, all in what surrounds it.
jobs type and none matched
tombstone, so the one run whose purpose ischanging how your profile looks to other people never said what it changed.
saved copy is written at login and nowhere else. The job now writes back the
value it reads off X to confirm the save, on the success path only.
about to succeed into a timeout.
""from both reads, and called a save that worked unchanged.
banner it already had and replaces it a moment later, so a successful save
read as no change. It is read now until it differs.
Watching the requests
CYD_REQUEST_LOG=/tmp/dir npm startrecords the method, URL and status ofwhat Cyd's own session sends. Off unless asked for. No bodies, headers or
cookies, and never attached to an error report — the reasoning ADR 0005
applies to archives applies here too.
It was built because the capture tooling drives a separate Chromium, so its
findings describe Chromium. That is fine until the app and the probe disagree,
which is where this stood: a banner that uploaded under Playwright and did not
under Electron, with nothing recorded on the Electron side to compare.
It answered that in minutes, and the answer was not what any of the DOM
theories predicted:
The image uploads; the call attaching it never happens. Apply is not a step X
sometimes puts in the way — it is what stages the upload as the banner. So a
delivery drawing no crop is made again, up to three times, and if X never
offers one that is the error, said plainly.
This also removes a test that asserted saving without a crop succeeds. It
passed, and it was wrong: nothing had watched what X was really sent.
Seeding
Found while re-seeding the test account.
/compose/postis a modal overthe home timeline, whose own composer carries the same test id, so the wait
retargeted to it the moment the modal closed. Every successful post was
reported as a failure and retried — a 41-post seed would have triple-posted.
timeline where acted-on posts leave the selector. 4 failures in 41 likes
before, 0 after acting on the first post still offering the action.
feed.
and never seeded. Accepted knowingly: no seeded post, no fixture, no marker.
Verification
Seeded @nexamind91326 and verified against the account, not the progress log:
41 posts, 41 likes, 41 bookmarks, 25 following, plus a self-thread, one-image,
four-image and video posts, a poll, a link card, 2 quotes and 2 retweets.
The delete run, read back out of the request log:
DeleteTweet51,DeleteRetweet2,UnfavoriteTweet42,DeleteBookmark42,friendships/destroy25 — every call 200, no rate limits, and the accountleft empty on every timeline. Zero direct-message API calls across 3,444
requests, which confirms against real traffic what
view_model.test.tsasserts.
The tombstone has since run three times in a row without failing, across two
accounts.
Suite: 119 files, 1499 tests, no type errors.
What merging this accepts
This closes #712, so it is worth being explicit about what the ticket's
remaining criteria rest on.
The delete run is verified above, against the request log and the resulting
account state. The tombstone has run three times without failing. The full
suite is green.
The save run, the un-loadable-timeline error report and the empty-account run
were exercised by hand rather than recorded here. The direct-message archive
criterion was struck from the ticket by decision, not by test: the data half
stays covered by
archiveBuild keeps saved conversations and messages in the archive, and the archive site's messages view remains untested by anything,which is a known and accepted gap.
🤖 Generated with Claude Code