Skip to content

ci: run the unit suite on Windows and smoke the Playwright sample there - #582

Open
chinmayajha wants to merge 3 commits into
mozarkai:mainfrom
chinmayajha:ci/windows-playwright-sample
Open

chinmayajha wants to merge 3 commits into
mozarkai:mainfrom
chinmayajha:ci/windows-playwright-sample

Conversation

@chinmayajha

@chinmayajha chinmayajha commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Closes #580

What Windows coverage was before

The installer CI in mozarkai/optics-framework-org runs on a real windows-latest runner and verifies install → optics --versionoptics list → idempotent re-run → uninstall. That proves the CLI starts on Windows. It does not prove the framework works there. The macOS and Linux journeys were walked by hand end to end; Windows had no equivalent, and the suite had never been run on it at all.

What this adds

1. The existing pytest job now runs on ubuntu-latest and windows-latest (fail-fast: false, so a Windows failure still reports the Linux result). This is the bulk of the value: ~1393 tests covering path handling, encoding and cleanup across the whole codebase, each reporting a precise failing assertion, rather than one opaque end-to-end step.

2. A playwright-sample job for the one thing unit tests cannot reach — a real browser launch writing real artefacts:

optics init demo --template playwright
optics doctor demo --check
optics execute demo

Each command is its own step, so any non-zero exit fails the job. execute does propagate failure — execute_main (optics_framework/helper/execute.py) calls sys.exit(1) when any test case is not PASS, and anything raised out of a subcommand is caught by cli.main's handler, which also exits non-zero.

Two things the exit code alone would not catch, so they are asserted explicitly:

  • _has_failed_results({}) is False, so optics execute exits 0 on an empty result set. The job reads execution_output/junit_output.xml and fails if it lists zero test cases, or any failure/error.
  • utils.save_screenshot wraps cv2.imwrite in a try/except, and cv2.imwrite returns False rather than raising on a rejected path. A write Windows refuses would be dropped silently and still exit 0. The job fails if no screenshot landed under execution_output/.

On failure, execution_output/ is uploaded as an artifact — the only way to diagnose a Windows-only failure from a non-Windows machine.

Both jobs live in tests.yml

Same triggers, same purpose, one checks list, one set of pinned action SHAs to maintain. #585 put its bare-image smoke in the same file for the same reason.

Why the e2e job skips pull requests

if: github.event_name != 'pull_request' — it runs on push to main and on workflow_dispatch.

The unit matrix now carries the regression-catching load, and it catches more, faster, with better failure messages. Against that, the e2e job downloads ~150 MB of Chromium and fetches a live example.com on every run: it would be the only network-dependent check in the repo, and making it a per-PR check couples PR throughput to a third party's uptime. It is also the slowest job here by a wide margin.

The trade is real and worth naming: a regression that only the e2e catches now surfaces on main rather than on the PR that caused it. Three things make that acceptable — the unit matrix covers the same defect classes at a finer grain, workflow_dispatch lets anyone run the e2e on demand before merging something risky, and main is still upstream of every release. If it turns out to catch things the matrix misses, moving it back to per-PR is a one-line change.

What the Windows matrix found: 28 failures, all of them test bugs

None were product bugs. Four harness assumptions, each of which passed on POSIX for the wrong reason (fixed in the first commit):

Root cause Tests
monkeypatch.setenv("HOME", ...) does not redirect expanduser on Windows — ntpath reads USERPROFILE, then HOMEDRIVE/HOMEPATH, and ignores HOME. The tests were asserting against the runner's real home. 11 (test_onboarding.py, test_doctor.py)
Constructing a prompt_toolkit Application resolves an output eagerly; with no Windows console that raises NoConsoleScreenBufferError. POSIX falls back to a vt100 writer, so no session was ever needed. 14 (test_live_keywords.py, test_live_driver_agnostic.py)
A / hardcoded in an assertion where the product correctly writes os.path.join. 2 (test_autocompletion.py)
python3 -m venv asserted where the product already, correctly, tells Windows python -m venv and Scripts\Activate.ps1. 1 (test_environment.py)

In the two cases where a platform difference was involved, the product was already the correct side of it. The Playwright browser-cache test also gains the Windows cache location — _playwright_browser_dirs has always listed AppData\Local\ms-playwright, but nothing covered it.

The e2e job is expected to stay red

It fails at optics doctor with:

Error: 'charmap' codec can't encode character '\U0001fa7a' in position 0

U+1FA7A is the stethoscope in doctor.py:498. On Windows sys.stdout is UTF-8 only when attached to a console; redirected (CI, optics doctor > log.txt, any build system) Python falls back to the ANSI code page and Rich writes the emoji straight through. Rich's safe_box degrades box-drawing to ASCII — visible in the optics init step, which printed +---+ — but it does not transcode emoji. It is unconditional: _STATUS_GLYPH (doctor.py:41) puts /⚠️/ on every row.

This needs no Windows runner to reproduce:

stream = io.TextIOWrapper(io.BytesIO(), encoding="cp1252")
Console(file=stream).print("🩺 optics doctor")   # UnicodeEncodeError

That fix, and the save_screenshot timestamp sanitisation (utils.get_timestamp() returns ...T16:51:50.397462+05:30, and utils.save_screenshot interpolates a caller-supplied timestamp into the filename unsanitised — colons are reserved on Windows), belong in the Windows-correctness PR, not in a CI change. This job goes green when that lands. The assertions are deliberately left intact rather than relaxed to make it pass.

@chinmayajha
chinmayajha force-pushed the ci/windows-playwright-sample branch from 112fc6a to 7e9e2e9 Compare September 22, 2026 20:40
@chinmayajha chinmayajha changed the title ci: run the Playwright sample on windows-latest ci: run the unit suite on Windows and smoke the Playwright sample there Sep 22, 2026
Chinmay Jha added 3 commits September 23, 2026 02:17
First Windows run of the suite: 28 failures, none of them product bugs.
Four harness assumptions, each of which quietly passed on POSIX.

`monkeypatch.setenv("HOME", ...)` does not redirect `expanduser` on
Windows — ntpath reads USERPROFILE, then HOMEDRIVE/HOMEPATH, and ignores
HOME — so the onboarding and doctor tests were asserting against the
runner's real home directory. `set_home` now sets both.

Constructing a prompt_toolkit `Application` resolves an output eagerly,
which on Windows with no console raises NoConsoleScreenBufferError; POSIX
falls back to a vt100 writer, which is why the live TUI tests never
needed a session before. They now run inside one bound to a DummyOutput.

The remaining two asserted on POSIX-shaped strings where the product is
already platform-correct: a `/` separator in the autocompletion source
line, and `python3 -m venv` where Windows is told `python -m venv` and
`Scripts\Activate.ps1`.

The Playwright browser-cache test also gains the Windows cache location;
`_playwright_browser_dirs` has always listed it, but nothing covered it.
Windows coverage stopped at `optics --version` and `optics list` in the
installer CI (mozarkai/optics-framework-org): proof the CLI starts, not
proof the framework runs. The suite has never been run on Windows, so
POSIX-separator assumptions, console encoding and rmtree behaviour were
all unverified.

Matrixing the job we already have is the cheapest way to cover that: it
reports a precise failing test instead of one opaque end-to-end step.
`fail-fast: false` so a Windows failure still reports the Linux result.
The unit matrix covers path handling and encoding, but nothing in it
launches a browser or writes a real artefact. This job scaffolds the
shipped Playwright sample, diagnoses it and runs it end to end, which is
the only way to reach `execution_output/` on Windows.

Two checks beyond the exit code, because neither would surface as a
failure on its own: `optics execute` exits 0 on an empty result set, and
`save_screenshot` swallows a rejected path because `cv2.imwrite` returns
False rather than raising. So the job reads back the JUnit report and
asserts a screenshot reached disk.

It skips pull requests: the matrix above carries the regression load,
and a ~150 MB Chromium download plus a live example.com fetch is a poor
trade on every PR. `workflow_dispatch` covers running it on demand.
@chinmayajha
chinmayajha force-pushed the ci/windows-playwright-sample branch from 7e9e2e9 to da926ec Compare September 22, 2026 20:47
@sonarqubecloud

Copy link
Copy Markdown

strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add macOS too

Comment on lines +71 to +98
run: |
$out = Join-Path 'demo' 'execution_output'
$report = Join-Path $out 'junit_output.xml'
if (-not (Test-Path -LiteralPath $report)) {
throw "No JUnit report at $report - the run wrote nothing under execution_output."
}
[xml]$junit = Get-Content -LiteralPath $report
$tests = 0; $failures = 0; $errors = 0
foreach ($suite in @($junit.testsuites.testsuite)) {
$tests += [int]$suite.tests
$failures += [int]$suite.failures
$errors += [int]$suite.errors
}
# `optics execute` exits 0 on an empty result set, so without this an
# empty report would read as a green run that executed nothing.
if ($tests -eq 0) {
throw "JUnit report lists no test cases."
}
if ($failures -ne 0 -or $errors -ne 0) {
throw "JUnit report lists $failures failure(s) and $errors error(s)."
}
# save_screenshot swallows a rejected path (cv2.imwrite returns False
# rather than raising), so a dropped capture would still exit 0.
$shots = @(Get-ChildItem -LiteralPath $out -Filter '*.jpg' -File)
if ($shots.Count -eq 0) {
throw "No screenshot under execution_output - capture dropped its output silently."
}
Write-Host "$tests test case(s) passed, $($shots.Count) screenshot(s) saved."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the comments, seems quite obvious

This branch has not been deployed

No deployments
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.

Windows coverage stops at optics --version

2 participants