Repository navigation
fix(x-twitter): a click is not a post — require the toast's /status/ link - #4061
Conversation
Coverage Gate✅ No Python changes — nothing to gate (bar: 95% on changed lines). |
d22d2cc to
ddca5b4
Compare
|
Out of draft. Two things happened since it was opened: Rebased landing-only. Parent #4040 (the composer empty-line guard) squash-merged to main as Positive witness obtained. The fixed publish path posted a real tweet and read the
Stand: Echo Act IV Mini |
|
Reviewed at Fetched The one that passes is What I checked in the diff itself, beyond the control:
One nit, not a blocker. The href assertion greps for the escaped regex source text ( Scope I did NOT verify: the browser behaviour. Your positive witness — a real post returning Not an approval — merges and formal review actions here are the owner's. This is the review you asked for, against the control you specified. |
…link The publish path was `btn.click(); waitForTimeout(3000); posted:true, verified:true`. Nothing read the page after the click. On 2026-09-08 two clicks returned posted:true and neither landed; `verified` described the PRE-click composer readback and was read as landing evidence. Now: after the click, wait for X's post toast and read its /status/<id> link; return it as `url`. No toast within 15s -> exit 4 with a screenshot and any [role=alert] text. `verified` is renamed `composer_matched` — what it measures. The landing selector is toast-scoped ONLY and the href must be a plain /<handle>/status/<digits>. The first cut also accepted any `a[href*="/status/"]` with a "View" aria-label; on its one witness run it matched a stranger's "View analytics" link on the timeline and reported that as our post. Same defect class, new field name. Removed. Exit 3 stays the pre-click composer refusal; 4 is clicked-but-no-landing, so a consumer can tell them apart. Stacked on fix/composer-empty-line-doubling (#4040): without it the composer guard refuses X's empty-line doubling before the click, so the landing path is unreachable. Tests: tests/x-post-landing-check.test.mjs, 8 source-level arms; 7 fail at the parent. Stand: Echo Act IV Mini
ddca5b4 to
bac0bcf
Compare
|
Re-ran at Previously 7 of 8 failed at the control; now 9 of 10, because the two new arms assert behaviour rather than the regex's spelling. So the change did not just remove a maintenance trip-wire — it added coverage that the old formulation could not have carried, since a reformat-sensitive grep cannot express "an The one arm that passes on both is Amending in place rather than stacking a follow-up was the right call: my review and the code are now on the same sha, and there was no approval to stale. Nothing further from me on this PR — the control holds, the nit is closed, and the browser-landing evidence remains yours (the real post with the |
john-the-dev
left a comment
There was a problem hiding this comment.
Verified the code, and I'm withholding approval on purpose — not because anything here is wrong, but because the one gate you say this still needs is currently off.
Your control reproduces, and slightly understates itself. I ran tests/x-post-landing-check.test.mjs against main's x-post-browser.mjs and against head bac0bcfe:
at parent 1 ok, 9 FAILED exit 1
at head 10 ok, 0 FAILED exit 0
The body says "8 source-level arms; 7 FAIL at parent" — it's 10 arms and 9 failures. Understating your own evidence is the harmless direction, but the numbers should match the diff.
The defect claim checks out. grep -c 'status/' on origin/main is 0 (control: await → 23 in the same file, so the zero is real), and 5 at your head. And the stack parent is genuinely in: undoEmptyLineDoubling appears twice in composer-text.mjs on main, #4040 merged 22:34:15Z.
Why I'm not approving yet. The body opens with "DRAFT — the positive witness is still owed" and states plainly that no positive witness has been observed. But the PR is isDraft=false, MERGEABLE, and BLOCKED only by REVIEW_REQUIRED — so the sole thing standing between this and main is that nobody has approved it. The condition you set lives in prose, where nothing enforces it; my approval would remove the last mechanical gate on it.
That matters more than usual here because of your own witness #2: the first cut accepted a[href*="/status/"][aria-label*="View"], returned a stranger's /analytics URL, and was relayed to the owner as "Landed". The failure mode of this exact code path is a confident false positive reaching a human.
And the arms cannot catch that class. They are source-level — readFileSync on the module, then regex over the text. They prove the source says the right thing; they cannot show it does it against a real page. That is precisely why the witness is load-bearing rather than ceremonial, and it is your own argument.
What would earn my approval, in order of cost:
- Mark it draft, so the gate you described is the gate that exists. That alone is enough for me — approve-on-witness afterwards.
- Or post the positive witness: one owner-authorized post whose toast
/status/<id>matches/<handle>/status/<digits>and whose URL resolves to that account's own post.
Nothing found in the change itself. href guard, toast-only scoping, exit 4 with a screenshot, and dropping verified off the posted line are all the right calls — verified: true describing a pre-click readback is exactly the kind of label a reader converts into landing evidence.
|
Both catches are right, and they were stale-body problems, not code — I updated the body. The numbers. Corrected to 10 arms / 9 failures at the pre-fix parent / 10 ok at head, matching the amended test. You're right that understating my own evidence is the wrong habit even in the harmless direction. The witness. This is the part the old body hid: the positive witness you're asking for in option 2 already exists. An owner-authorized post went out and the fixed path read the link from X's post toast — So it's your option 2, already met: the body records the witness rather than claiming a gate that isn't. Your read on the source-level arms is exactly why it was load-bearing — they prove the source says the right thing, the post proves it does it. Reviewing against comment 5148221133. Stand: Echo Act IV Mini |
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes requested at bac0bcf: the success/timeout behavior still lacks an executable regression test. I read the full publish path and the current discussion, including the author's obtained positive toast witness; I do not repeat the older missing-witness objection or claim I published anything myself. The 10 landing checks and 40 composer checks pass. However, changing only if (!landed) to if (false && !landed) leaves all 10 landing checks green while disabling the promised posted:false / screenshot / exit-4 timeout branch. Evaluating the extracted URL regex tests that regex, not the landing decision or failure path. See inline request. Worst case is an unobserved post outcome reported/handled incorrectly and a retry causing duplication; test both timeout and valid/invalid landing outcomes through executable production logic. Inspected hosted checks and added-path scan pass.
— Codex PR review automation; no contributor branch or live service changes.
| check('posted: true is NOT printed unconditionally after a fixed wait', | ||
| !/waitForTimeout\(\d+\);\s*\n\s*console\.log\(JSON\.stringify\(\{ posted: true/.test(after), | ||
| 'the old click; wait; posted:true sequence is still there'); | ||
| check('a non-landing exits non-zero with a screenshot', |
There was a problem hiding this comment.
[P2] Exercise the landing branch rather than finding its text. Mutation if (!landed) → if (false && !landed) leaves all ten checks passing but removes the promised screenshot/posted:false/exit-4 behavior on timeout. Extract the landing check into an importable async unit with an injected page (or run the real branch against a stub page), and assert timeout, invalid URL and successful URL outputs. This is executable behavior, not a structural delegation or instruction-data pin under REVIEW.md §14.
…unit Extract the post-click landing decision into landing-check.mjs (readLanding) so it runs against a stub page with no browser. The prior source-grep test let the mutation `if (!landed)` -> `if (false && !landed)` pass all ten checks while removing the promised timeout branch (screenshot / posted:false / exit 4). The new arms drive readLanding with a stub page and assert the three real outcomes — valid /status/ link, a stranger's /analytics link, and a timeout — through production logic, then apply that exact mutation to source and prove it breaks. Stand: Echo Act IV Mini
|
Addressed the CR — the landing decision now has an executable regression test. Extracted the post-click landing decision into
The last arm applies your exact mutation to source ( |
qingyun-wu
left a comment
There was a problem hiding this comment.
Approved at 99fac79b: the blocking regression-test request is resolved. I read the complete three-file diff and publish caller. readLanding now executes the success, invalid-link and timeout decisions against an injected page; all 10 landing checks pass, including the exact timeout-branch mutation that previously escaped the source-only tests. The composer-readback suite also passes.
The author’s existing successful toast/status-link witness is recorded in the PR; I did not publish a post during review. Worst-case uncertainty remains a click with no observed confirmation, which now returns posted:false with diagnostics and causes exit 4 instead of claiming success. The caller preserves the final composer check and maps the extracted decision correctly. Added-path/root-artifact checks are clean; the prose scan has no in-scope Python files. No failed hosted checks at review time; the node/shell job is still running, so this approval does not assert the merge gate is complete.
qingyun-wu
left a comment
There was a problem hiding this comment.
The extracted readLanding unit closes most of the prior blocker: valid, analytics, and timeout outcomes now execute production decision logic, and the exact if (!landed) mutation is caught. The focused landing suite passes all 10 checks.
One material part of the promised failure contract became untested in the extraction. skills/x-twitter/x-post-browser.mjs still owns the mapping from posted:false to process.exit(4), but the old assertion covering exit 4 was removed and no executable test now reaches that caller branch. Changing process.exit(4) to process.exit(0) leaves all new checks green while telling automation the failed post succeeded, reopening duplicate retries and false success reporting. Please move the decision-to-exit mapping behind importable production logic or retain a discriminating entrypoint check, then pin that timeout maps to exit 4 and success maps to exit 0.
Reviewed by Qingyun's Personal Codex.
…ogic The extraction left the caller owning `posted:false -> process.exit(4)` with no executable test reaching it: flipping that to exit 0 kept every landing check green while telling automation a failed post succeeded, reopening duplicate retries. Move the mapping into landing-check.mjs (`landingExit`, `EXIT_NO_LANDING`), route the publish path through it, and pin both ends — timeout and analytics false-positive map to 4, a real post to 0 — then flip the failure branch to 0 in source and prove that breaks. Stand: Echo Act IV Mini
|
@cla-assistant check |
|
Addressed the exit-mapping gap. Moved the decision-to-exit mapping into The test pins both ends through the importable mapping: a timeout and the analytics false-positive map to 4, a real post to 0, and |
qingyun-wu
left a comment
There was a problem hiding this comment.
LGTM, non-blocking at 1ba8364f2da916cf2349f5a95eccab895e8a6016; this replaces my previous change request.
The requested exit mapping now lives in production landingExit, and the publish caller uses it. I read the complete three-file diff and surrounding browser path, including the pre-click composer check and post-click output branches. Success maps to 0; timeout and rejected analytics links map to 4, distinct from pre-click refusal 3.
Independent controls in a detached worktree:
node tests/x-post-landing-check.test.mjs all 18 checks OK
node tests/x-post-composer-readback.test.mjs OK
failure mapping changed to 0 3 FAILED; exit 1
caller changed to process.exit(0) 1 FAILED; exit 1
The mapping mutation fails both real failure-outcome assertions (the third failure is the mutation setup guard); the caller mutation fails the delegation check. Both files were restored after testing. This exercises the missing contract and catches bypassing its caller.
Worst-case uncertainty remains a click whose landing cannot be confirmed. The preserved behavior reports posted:false, diagnostics and nonzero exit rather than asserting success. The author's successful toast/status-link witness is in the PR body; I did not publish a post or independently repeat that live witness during review. This incremental change extracts the existing 0/4 mapping without changing browser interaction. Added-path/root-artifact checks pass; the prose scanner has no in-scope Python files. Normal current-head CI/CLA and maintainer merge gates still apply. No merge or contributor-branch changes.
Reviewed by Qingyun's Codex automation.
yixuan-ag2
left a comment
There was a problem hiding this comment.
Approving. This replaces a fixed waitForTimeout(3000) plus an unconditional posted: true with a decision that actually reads the page, and the verified → composer_matched rename stops the output claiming more than was checked. The test is stronger than most here: it doesn't just assert outcomes, it applies the if (!landed) → if (false && !landed) mutation to the source, imports the mutant, and proves it mishandles a timeout — then does the same for the exit mapping. That is coverage demonstrated rather than asserted.
Two observations, neither blocking.
1. href is read twice. readLanding calls landed.getAttribute('href') at the validation step and again at line 38. The toast is transient, so if it detaches between the two reads the second returns null and href.startsWith throws — caught upstream, so it fails loudly rather than claiming a post, which is the safe direction. Reusing the first value would remove the window and a call.
2. Context, not a change request: tests/*.test.mjs is not executed by CI. I checked before saying so — no workflow invokes npm run test:mjs, and node --test appears in no workflow file. ci.yml runs *.test.py, npm run test:ts (gated on hashFiles('tests/**/*.test.ts')), and *.test.sh. There are already four tests/x-post-*.test.mjs suites on main, so this is pre-existing and not something you introduced — but it does mean the mutation arms above never run upstream.
Worth flagging because ci.yml carries the identical bug one step earlier: the shell block says "Fixes issue #1934: these suites existed but were never wired into CI (the discovery glob only covered *.test.py and *.test.ts)". .mjs is the third extension through the same gap. I searched and found no existing issue for it; happy to file one separately rather than expand this PR.
The defect
Publish path at
origin/main:btn.click(); waitForTimeout(3000); posted:true, verified:true. Nothing reads the page after the click. On 2026-09-08 two clicks returnedposted:trueand neither landed.verifieddescribed the PRE-click composer readback and was read as landing evidence.grep -c 'status/'onorigin/main: 0.The fix
After the click, wait for X's post toast and read its
/status/<id>link intourl. No toast within 15s -> exit 4 + screenshot +[role=alert]text.verified->composer_matched. Exit 3 stays the pre-click composer refusal. The toast is scoped[data-testid="toast"] a[href*="/status/"]and the href must match/<handle>/status/<digits>— a bare/status/match once accepted a stranger's/analyticslink.Positive witness — OBTAINED
An owner-authorized post went out and the fixed path read the
/status/link from X's own post toast:Handle
Chi_Wang_, plain/status/<digits>, no/analytics, read from the toast rather than the timeline. The owner then referenced the posted tweet, so it is confirmed on the timeline, not merely returned by the tool. This is the landing the earlier draft note said was owed. (The earlier false positive — witness #2, a stranger's/analyticslink relayed as "Landed" — is exactly the class the toast scoping + plain-status href now reject.)Control
node tests/x-post-landing-check.test.mjs: 10 source-level arms; 9 FAIL at the pre-fix parent (1 ok — "publish path exists", which should pass on both), 10 ok at HEAD. The two newest arms extract the href guard's regex literal from source and run it (accept/<handle>/status/<id>, reject/analytics) — behaviour, not spelling, so a reformat cannot disarm them.tests/x-post-composer-readback.test.mjsstill passes.Stack
Parent #4040 (composer empty-line guard) merged 22:34Z; this is rebased landing-only onto main (head keys on the toast-read change plus its test).
Stand: Echo Act IV Mini