Studio CI: make a failing browser smoke say why, and stop it skipping the build gates by danielhanchen · Pull Request #8983 · unslothai/unsloth · GitHub
Skip to content

Studio CI: make a failing browser smoke say why, and stop it skipping the build gates - #8983

Merged
danielhanchen merged 18 commits into
mainfrom
fix-ansi-smoke-diagnostics
Aug 18, 2026
Merged

Studio CI: make a failing browser smoke say why, and stop it skipping the build gates#8983
danielhanchen merged 18 commits into
mainfrom
fix-ansi-smoke-diagnostics

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Aug 16, 2026

Copy link
Copy Markdown
Member

"Browser smoke for ANSI tool output" is intermittently red. Across the runs I have watched reach the step, it failed 8 times and passed 3, on main and on branches alike, and it carries no continue-on-error, so every step behind it is skipped whenever it goes.

This does not fix the smoke. It makes the smoke able to tell us what is wrong, and it stops the smoke deciding whether the build gates run.

What the failure looks like today

Run 31935573269, main, in full:

[ansi-smoke] starting vite dev server on port 5203
[ansi-smoke] http://127.0.0.1:5203/smoke-ansi.html ready (serves smoke-ansi-main.tsx)
AssertionError: Locator expected to be visible
  - waiting for locator("section[data-smoke=\"tool-result-output\"] pre").first

That is everything. No console, no page error, no dev-server output, no screenshot kept. An entry module that threw and an entry module that was merely slow produce exactly that text, and the two need opposite fixes.

I could not reproduce it here: 7 clean runs including a cold vite dependency cache and a run pinned to two cores. So the next step has to be evidence from the runner, which the harness currently discards.

Change

Say why. echo_browser_errors(page, info) in _playwright_robust.py prints pageerror, console.error, requestfailed and main-frame navigations as they arrive. All three smokes get it; none of them listened for any of it before. On failure the ANSI smoke also calls the existing dump_diagnostics (screenshot, URL, body excerpt) and prints vite's own tail, which is the only place a transform error or a forced reload is reported.

Verified by injecting throw new Error(...) at module scope in smoke-ansi-main.tsx. Before, that is indistinguishable from the log above. After:

[ansi-smoke] navigated: http://127.0.0.1:5203/smoke-ansi.html
[ansi-smoke] pageerror: simulated module-scope crash
[ansi-smoke] vite tail:
[ansi-smoke]   10:18:56 AM [vite] (client) [Unhandled error] Error: simulated module-scope crash
[ansi-smoke]    > smoke-ansi-main.tsx:13:6

Stop it gating the build. The three smokes move below Build, the unstable_Provider assertion and the 75 MB budget. A job stops at its first failing step, so today one red smoke means the checks that decide whether the app ships never report at all. The smokes each start their own vite dev server and read nothing out of dist/, so the order is free. The smokes stay blocking; they just fail on their own account now.

Keep the evidence. actions/upload-artifact on logs/playwright-*, if: failure() only, 3 day retention. Small, unlike the 35 MB dist upload.

Coverage

Three assertions in tests/studio/test_autoscroll_harness_contract.py, the existing file for "a harness must read what it records":

  • all three smokes attach the listener, so a crashed page can never read as a timeout again
  • the ANSI assertions run under the dump, rather than beside it
  • vite's output is not dropped on failure

Testing

  • python3 -m pytest tests/studio/test_playwright_server_lifecycle.py tests/studio/test_autoscroll_harness_contract.py -q: 25 passed
  • playwright_strip_ansi_smoke.py: passes, and fails with a named cause under the injected crash
  • playwright_research_freeze.py: passes
  • playwright_chat_autoscroll.py: fails here with "the content did not grow while detached", identically with and without this branch (checked by stashing it), so it is pre-existing and untouched
  • ruff check tests/studio: clean

What this does not do

It does not fix the smoke, because I still cannot say what is wrong with it. Seven clean local runs, including one with a cold vite dependency cache and one pinned to two cores, did not reproduce it. It is not deterministic in CI either: it passed today on #8980, a branch whose only difference from main is three test-file path fixes that the smoke does not touch. An intermittent failure with no console, no page error and no server output is not diagnosable, which is what this PR changes.

A green run of this branch therefore proves nothing on its own. The value is the next red one.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@danielhanchen

Copy link
Copy Markdown
Member Author

@danielhanchen

Copy link
Copy Markdown
Member Author

Correction to my earlier comment. I merged #8980 in to get past a red Unit tests on main, but #8981 landed that same fix independently at d43892ea7, so the reason is gone. I have merged main instead and dropped the #8980 content, so this branch's diff is its own again. #8980 is now Windows-only and unrelated to this.

danielhanchen added a commit that referenced this pull request Aug 17, 2026
…9026)

* Repair the prompt-queue contract test against the queueing refactor

Repo tests (CPU) is red on main. test_composer_only_queues_behind_the
_current_chat greps thread.tsx for code that #8952 moved or replaced, so
it fails on any branch regardless of what that branch changes. It is
currently red on #8935, #8964, #8980 and #8983 for this reason alone.

The behaviour it guards is intact, and in two of the three cases the
code that replaced it is stronger than what the test still asserted, so
the assertions are repointed rather than dropped:

- Queueing moved out of handleSubmit into an extracted queueComposerText,
  so the Cmd/Ctrl+Enter path could share it. Assert the delegation in
  handleSubmit and the queueing inside queueComposerText, which keeps
  this a contract on behaviour rather than on where the code sits.

- promptQueueStartPendingRef.current.has(reservationKey) became a
  .get(reservationKey) === identity comparison. A reservation can be
  replaced between the start and the callback, and acting on the
  successor would dispatch the wrong prompt; presence alone never
  caught that.

- temporary: useChatRuntimeStore.getState().incognito became
  temporary: incognitoAtQueueStart, captured when the queue starts
  instead of read live at dispatch. A chat toggled out of temporary
  mid-queue must not have its queued prompts persisted.

Each rewritten assertion was checked against a deliberately broken tree
rather than assumed to discriminate. Removing the delegation, swapping
the identity check back to presence, reading temporary live again, and
stopping queueComposerText from queueing each fail the file.

tests/studio 11 passed for this file, whole directory green.

* Read the identity check out of the dispatch guard, not the whole file

Reported on this PR and correct. The assertion searched thread.tsx for
promptQueueStartPendingRef.current.get(reservationKey) === anywhere, and the
abort and cleanup branches beside the dispatch carry the same comparison. The
dispatch guard could regress to .has(reservationKey) on its own, which is exactly
the bug this assertion exists to catch, and the test would still pass on its
neighbours.

It now slices the if condition that guards the startPromptQueue call and asserts
the identity comparison there, with .has excluded from that guard. All three
comparisons are still required by count, because the other two are load-bearing
too: abort without it reports the successor's start as this one's failure, and
cleanup without it deletes the successor's entry.

11 passed. Rewriting the dispatch guard to .has fails it; it passed before.

---------

Co-authored-by: danielhanchen <unslothshared@gmail.com>
chatgpt-codex-connector[bot]

This comment was marked as resolved.

pre-commit-ci Bot and others added 2 commits August 18, 2026 03:22
… gate ahead of the smokes

The failure dump iterated the live deque the drain thread is still appending to,
while writing each line to stdout, so a vite server that was still talking during
the dump raised deque mutated during iteration and dropped the tail in exactly the
noisy failure the dump was added for. list() of a deque is atomic; take it first.

Startup bundle budget still ran after the browser smokes, and every step carries an
implicit success(), so a red smoke skipped it. It only needs dist/, so it moves up
with the other build gates.
@danielhanchen

Copy link
Copy Markdown
Member Author

On the red check: (Python 3.13) in Backend CI is inherited from main, not produced by this branch.

The failing assertion is

FAILED studio/backend/tests/test_text_io_encoding.py::test_text_io_names_its_encoding[lan_access.py]
assert not ['lan_access.py:127: subprocess(text = True) without encoding']

studio/backend/lan_access.py and studio/backend/tests/test_text_io_encoding.py are both byte-identical between this branch and origin/main, neither is in this PR's file list, and the same job fails identically on main at e4631945. The fix belongs on main and is open as #9142.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 65501849bc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@danielhanchen

Copy link
Copy Markdown
Member Author

Cross-platform run

Replicated onto a staging repo rather than adding to the org queue, branched from fresh upstream main and merged with this PR's head.

job result
ubuntu-latest success
windows-latest success
macos-14 success
studio-playwright success
Frontend CI success
Mac Studio UI + API + Update CI success
Workflow trigger lint success
Backend CI failure, (Python 3.13) / Backend tests

The one red is the inherited lan_access.py encoding failure that main carries at e4631945, reproduced here on a tree that is main plus this PR, which is the cleanest confirmation available that it is not this branch's. It is fixed in open PR #9142.

Workflow trigger lint passing matters for this PR specifically, since it edits .github/workflows/studio-frontend-ci.yml. I also parsed the file with yaml.safe_load and re-read the resulting step order to confirm the four build gates now sit at indices 12 to 15 and the browser smokes at 18 to 20, rather than trusting the reorder.

The staging PR is closed, never merged.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: 7b2008209d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

# Conflicts:
#	.github/workflows/studio-frontend-ci.yml
#	tests/studio/test_autoscroll_harness_contract.py
@danielhanchen

Copy link
Copy Markdown
Member Author

Merged main in and resolved the conflict

main moved (8969, 9048) and this branch went CONFLICTING. Two files conflicted, and one of them is the conflict that matters for this PR.

.github/workflows/studio-frontend-ci.yml, a semantic conflict, not a textual one. main added a new Browser smoke for chat stream pacing step and keeps Build after the smokes. This PR moves the build gates ahead of the smokes. Resolving either side alone would have silently undone one of them.

I kept both: this PR's ordering, and main's new step placed with the other smokes. Final order re-parsed from the merged file rather than assumed:

index step
12 to 15 Build, unstable_Provider, Bundle size budget, Startup bundle budget
16 to 17 Install Chromium, Dev-server lifecycle tests
18 to 21 ANSI, autoscroll, research freeze, chat stream pacing (continue-on-error)
22 Upload browser smoke artifacts (if: failure())

main's new tests/studio/playwright_stream_pacing.py path trigger is kept too. Nothing dropped from either side.

tests/studio/test_autoscroll_harness_contract.py, purely additive. This PR adds three test functions, main adds two, at the same place. I kept every one and checked it rather than eyeballing: the merged file has 11 test functions, and the set difference against each side is empty in both directions, so no assertion was dropped from either.

Re-proved after the merge, because a merge that quietly neuters a guard is the failure mode worth guarding against. pytest tests/studio/test_autoscroll_harness_contract.py tests/studio/test_playwright_suites_run_in_ci.py is 16 passed. Then, on a deliberately broken tree, I moved Startup bundle budget back behind the browser smokes and test_no_build_gate_sits_behind_a_browser_smoke failed as it should, and passed again once restored. The guard still bites, and it correctly does not fire on main's new smoke, which sits after the gates.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: ecbb5fdd33

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

# Conflicts:
#	.github/workflows/studio-frontend-ci.yml
@danielhanchen

Copy link
Copy Markdown
Member Author

Merged main in again after 8962, 8966, 8979, 9087, 9091 and 9142 landed

Same conflict as last round and the same semantic trap, now one step larger. main keeps Build after the browser smokes; this PR moves the build gates ahead of them. main's smoke block has also grown two new steps from 8966.

I kept both sides. The gates stay ahead, and main's two new settings smokes were carried down into this PR's relocated block alongside the stream-pacing one:

index step
10 to 13 Build, unstable_Provider, Bundle size budget, Startup bundle budget
14 to 15 Install Chromium, Dev-server lifecycle tests
16 to 21 ANSI, autoscroll, research freeze, settings tab panels, settings panel that cannot load, chat stream pacing
22 Upload browser smoke artifacts (if: failure())

Verified by set difference rather than by reading: no step name and no pull_request.paths entry from either side is missing. Nothing dropped.

Re-proved after the merge. pytest tests/studio/test_autoscroll_harness_contract.py tests/studio/test_playwright_suites_run_in_ci.py is 16 passed; the wider enumerating suites (test_no_test_shadows_another.py, test_playwright_server_lifecycle.py, test_smoke_workflows_share_one_script.py) bring it to 43 passed. Then on a deliberately broken tree I moved Startup bundle budget back behind the smokes and test_no_build_gate_sits_behind_a_browser_smoke failed, and passed again once restored. The guard still bites, and it correctly does not fire on any of main's three new smokes, which all sit after the gates.

@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 18, 2026
danielhanchen added a commit to shimmyshimmer/unsloth-staging-4 that referenced this pull request Aug 18, 2026
chatgpt-codex-connector[bot]

This comment was marked as resolved.

The stream-pacing smoke is continue-on-error, which rewrites its CONCLUSION
to success while leaving its OUTCOME as failure. The artifact upload was
gated on a bare failure(), so on the runs where that smoke was the only thing
that failed -- exactly the runs where its report is the whole point -- the
upload was skipped and logs/playwright-stream-pacing went nowhere.

Give the step an id and OR its raw outcome into the upload condition.

Guarded: the new test walks every continue-on-error browser smoke and fails
if it has no id, or if its outcome is not named in the upload condition.
Shown red two independent ways first, reverting the condition to bare
failure() and separately deleting the step id, each naming the step.
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 18, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 18, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 1c86c03e95

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 18, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 18, 2026
@danielhanchen

Copy link
Copy Markdown
Member Author

Merged current main (3bf9bff5d) into this branch at 407fd8c0f. The branch was eight commits behind and had gone red on Chat UI Tests (banner); that failure is #9150's, which is now on main, so the merge clears it rather than papering over it.

No conflicts. Nothing in the merge touches this PR's own change (the build-gate ordering in studio-frontend-ci.yml and the two upload-coverage assertions).

Re-proved one guard red on the merged tree before pushing: reverting the upload step's if: back to a bare failure() turns test_a_continue_on_error_smoke_can_still_upload_its_report red, and restoring failure() || steps.stream_pacing.outcome == 'failure' turns it green again. Full file at the merge commit: 7 passed; the wider set (test_playwright_suites_run_in_ci.py, test_autoscroll_harness_contract.py, test_no_test_shadows_another.py) 19 passed.

@danielhanchen

Copy link
Copy Markdown
Member Author

Simulation evidence for this PR. The claim is narrow, so I tried to break it in the ways a real CI failure actually arrives rather than only the one that motivated it.

The original failure, reproduced then fixed

Five ways a browser smoke can fail, each run twice: once on the merge base and once on this branch, same worktree layout, same Chromium, same injected fault. What CI would have printed:

injected fault base output this branch artifacts base / head
module-scope throw in the entry 48 lines, ends at element(s) not found 72 lines, names the throw 1 / 3
vite serves the SPA fallback, entry missing 13 lines 13 lines, already explicit 0 / 0
vite.config.ts throws, dev server never starts 32 lines 32 lines, already explicit 0 / 0
backend never answers /api/health 61 lines 84 lines, shows the proxy errors 1 / 3
transform-error storm plus HMR churn 48 lines, ends at element(s) not found 77 lines, shows the 500s and the aborts 1 / 3

The three modes that improve are exactly the three where the base output stopped at a Playwright locator timeout with no statement of cause. On this branch the same run additionally prints pageerror: simulated module-scope crash and a tail of the vite server log, which for the throw case includes the offending source line:

[ansi-smoke] pageerror: simulated module-scope crash
[ansi-smoke] vite tail:
[ansi-smoke]   [vite] (client) [Unhandled error] Error: simulated module-scope crash
[ansi-smoke]    > smoke-ansi-main.tsx:13:6
[ansi-smoke]       13 |  throw new Error("simulated module-scope crash");

For the storm case it prints the 500 and the aborted module request that caused it, and for the health-timeout case the repeated http proxy error: /api/health ECONNREFUSED. None of that reached the log on base.

The two modes that do not improve are honest negatives and I am reporting them as such: the SPA-fallback and dead-config cases already raised a specific RuntimeError naming the URL and the missing entry, so this PR adds nothing there and the output is byte-identical apart from the worktree path.

Artifacts also go from 1 file to 3 on every failing mode that reaches the browser: base wrote only smoke-ansi.png, this branch also writes smoke-ansi-failure.png and smoke-ansi-failure.json recording url, title, body excerpt and localStorage keys at the moment of failure.

Cross engine

The green arm and the throw arm were both run on Chromium, Firefox and WebKit. Green arm: all three exit 0 and all five production panes render clean text with no ANSI escapes. Throw arm: all three exit 1 and all three print the pageerror line plus a vite tail. Firefox and WebKit report the throw at slightly different columns (13:6 versus 13:16), which is a frame-formatting difference between engines, not a difference in what the smoke captures. Playwright's WebKit is a proxy for Safari here and is not Safari; nothing in this evidence is a claim about shipping Safari.

Signals and cleanup

SIGTERM and SIGINT were sent mid-run. Neither leaves a vite server or a browser behind; SIGINT surfaces the usual KeyboardInterrupt plus a TargetClosedError from the in-flight assertion, which is asyncio teardown noise rather than a leak.

The reordering half

The other half of this PR moves the four build gates ahead of the browser smokes. Verified on this branch: npm run build exits 0 at its new position (step 12), npm run bundle:check exits 0 at its new position (step 15) and reports 135.9 KB transfer and 502.4 KB raw to spare, and the build writes nothing outside dist/ (708 paths under dist, zero elsewhere).

Assertions proven red

Both new assertions were shown to fail on a deliberately broken tree, on the merged tree at 407fd8c0f:

  • reverting the upload step's if: to a bare failure() turns test_a_continue_on_error_smoke_can_still_upload_its_report red; restoring failure() || steps.stream_pacing.outcome == 'failure' turns it green.
  • moving Startup bundle budget back behind the browser smokes turns test_no_build_gate_sits_behind_a_browser_smoke red.

Staging CI

Replicated onto a staging repo rather than adding load to the org queue. That staging run was created at 05:03 UTC from PR head 47af708bc, which predates #9150 landing on main, so its Chat UI Tests (banner) red is the stale base and not this branch. This branch has since been merged up to main at 407fd8c0f; the authority for green is now this PR's own checks, not that staging run.

GPU matrix

Not applicable rather than skipped. This PR changes a GitHub Actions workflow and two Python assertion files. There is no CUDA, dtype or model axis to take a cartesian product over.

Old installs

Nothing here is installed, persisted or migrated. No change to any file an existing ~/.unsloth/studio reads or writes.

# Conflicts:
#	.github/workflows/studio-frontend-ci.yml
@danielhanchen

Copy link
Copy Markdown
Member Author

Resolved the conflict with #9016 at fe420d02a. One file conflicted, .github/workflows/studio-frontend-ci.yml, and it conflicted for the obvious reason: this PR moves the browser-smoke block below the four build gates, and #9016 added new steps to that block where it used to sit.

Nothing was dropped from either side. What I kept:

  • This PR's ordering. The four build gates (Build, unstable_Provider call site, Bundle size budget, Startup bundle budget) stay ahead of every browser smoke. That is the whole point of the change: a broken build should not wait tens of minutes behind a browser suite to tell you.
  • All of Studio: measure where a heavy thread stalls, across engines and thread size #9016's new steps, moved to the relocated block rather than left in the old position. Browser smoke for heavy-thread interaction cost in full, with its SMOKE_HEAVY_CHARS / SMOKE_HEAVY_ENGINES / SMOKE_HEAVY_REPEATS / PW_ART_DIR env and its comment about why the PR gate runs two sizes and one repetition. And the three new contract tests added to the Dev-server lifecycle tests pytest list: test_heavy_thread_harness_contract.py, test_heavy_thread_measurement_integrity.py, test_heavy_thread_gap_contract.py.

No assertion from either side was dropped, and nothing was weakened to make the merge go through.

Verified on the merged tree, not assumed:

  • YAML parses, both jobs (build, windows) intact.
  • test_playwright_suites_run_in_ci.py, test_autoscroll_harness_contract.py, test_heavy_thread_harness_contract.py, test_heavy_thread_measurement_integrity.py, test_heavy_thread_gap_contract.py, test_no_test_shadows_another.py: 112 passed.
  • Guard re-proved red rather than taken on trust: moving Startup bundle budget back behind the browser smokes turns test_no_build_gate_sits_behind_a_browser_smoke red, and restoring it turns it green. The tree was confirmed clean before and after.

The step order now reads: Unit tests, Build, unstable_Provider, Bundle size budget, Startup bundle budget, Install Chromium, Dev-server lifecycle tests, then the six browser smokes, then the failure-artifact upload.

@chatgpt-codex-connector

Copy link
Copy Markdown

@danielhanchen
danielhanchen merged commit aa32c18 into main Aug 18, 2026
24 of 26 checks passed
@danielhanchen
danielhanchen deleted the fix-ansi-smoke-diagnostics branch August 18, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant