Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f7943de
fix(save): bind exports to document and conversion ownership
chaxus Oct 18, 2026
7e287e1
fix(editor): publish document metadata only after mounting
chaxus Oct 18, 2026
c1c6e75
fix(agent): preserve incomplete provider responses and pause queues
chaxus Oct 18, 2026
268cce3
fix(agent): fit document excerpts within the remaining context budget
chaxus Oct 18, 2026
49bb0cf
fix(save): install conversion tracking before cold exports
chaxus Oct 18, 2026
1ca6e7f
fix(offline): return a network error for uncached offline assets
chaxus Oct 18, 2026
a9bef81
test(browser): keep API fixtures reachable under worker control
chaxus Oct 18, 2026
371f895
ci(browser): retain reports and run serial cases after failures
chaxus Oct 18, 2026
3061e57
perf(ci): balance browser shards using measured suite durations
chaxus Oct 18, 2026
127f985
fix(gemini): normalize filtered and continuation stop reasons
chaxus Oct 18, 2026
d0e715e
fix(agent): refit read excerpts as tool exchanges grow
chaxus Oct 18, 2026
b5f0841
fix(agent): reject incomplete responses across direct generation paths
chaxus Oct 18, 2026
9b1f62a
fix(save): retain export ownership through native pagination
chaxus Oct 18, 2026
1959119
test(ci): validate duration-based shard workflow contracts
chaxus Oct 18, 2026
26fdc30
fix(gemini): reject abnormal candidate termination before execution
chaxus Oct 18, 2026
e07b103
test(browser): hold native scroll keys across animation frames
chaxus Oct 18, 2026
b8968fc
docs: record reliability fixes and browser CI verification
chaxus Oct 18, 2026
be26bb8
test(browser): preserve logical API events across HTTP fixtures
chaxus Oct 18, 2026
3d85baf
fix(assistant): preserve composer focus across native editor entry
chaxus Oct 18, 2026
d531936
fix(assistant): contain native progress borders within the loading row
chaxus Oct 18, 2026
b014008
test(save): compare download names against native browser policy
chaxus Oct 18, 2026
c534254
docs: record cross-browser root causes and final regression results
chaxus Oct 18, 2026
aef3318
test(browser): hold model downloads under service-worker control
chaxus Oct 18, 2026
3634ebe
perf(ci): run nightly browser engines on independent runners
chaxus Oct 18, 2026
deacfb4
test(browser): serve failed document opens through HTTP fixtures
chaxus Oct 18, 2026
a4c5526
test(browser): preserve native save and artifact download contracts
chaxus Oct 18, 2026
5095714
test(browser): hold startup history through stable IndexedDB hooks
chaxus Oct 18, 2026
eed62fc
fix(assistant): retain focus ownership across native SDK callbacks
chaxus Oct 18, 2026
e7c74dd
fix(ci): install system dependencies for cached browser engines
chaxus Oct 18, 2026
de6a604
test(browser): bind model cache fixtures to the ready editor
chaxus Oct 18, 2026
052cead
test(save): verify exact text before overlapping conversions
chaxus Oct 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 29 additions & 47 deletions .github/scripts/install-playwright.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,43 +17,34 @@ if [ ${#browsers[@]} -eq 0 ]; then
browsers=(chromium)
fi

# On a cache hit, skip apt entirely.
#
# What `install-deps` actually does on a hosted runner, read off a healthy run:
# every library Chromium needs -- libnss3, libgbm1, libasound2t64, libcairo2,
# the lot -- reports "is already the newest version". The runner image ships
# them. The only thing apt installs is 21.1 MB of fonts (fonts-wqy-zenhei,
# fonts-ipafont-gothic, fonts-unifont, fonts-freefont-ttf, fonts-tlwg-loma-otf,
# xfonts-encodings), for rendering scripts a screenshot might contain.
#
# This suite does not render through them. The editor draws with its own
# vendored XOR font catalog (public/fonts/), PDF export injects
# PDF_FONT_MANIFEST, and the landing pages load vendored Geist woff2 -- system
# fonts reach nothing but DOM fallback text. The visual specs compare two
# renders from the same browser (original against saved-and-reopened), so a
# missing glyph would be missing identically on both sides.
#
# So each job was making a network call to Ubuntu's mirrors, on a fresh VM,
# for fonts nothing reads. Eleven jobs, eleven rolls of the dice per run -- and
# the dice are loaded: the runner's preferred azure.archive.ubuntu.com comes
# back `Ign:`, apt falls through to the public archive, and that stalls dead
# with no bytes moving. Four jobs were killed at GitHub's six-hour limit this
# week before the attempts were bounded; two more burned 17 minutes each on a
# single pull request after that.
#
# A cold cache still installs, deps and all: there are no browsers to run
# without it, and that path downloads from Playwright's CDN anyway.
# Browser binaries are cached; system libraries live only on the current runner.
# Chromium's libraries ship on the hosted image, so retain its font-only fast
# path. WebKit/Firefox need additional libraries (for example libevent for WPE)
# and must install them even when their browser binaries came from the cache.
needs_system_deps=false
for browser in "${browsers[@]}"; do
if [ "$browser" != "chromium" ]; then
needs_system_deps=true
fi
done

if [ "${BROWSERS_CACHED:-}" = "true" ]; then
if [ "${PLAYWRIGHT_INSTALL_DEPS:-}" != "true" ]; then
echo "Browsers restored from the cache; skipping the apt font install."
exit 0
if [ "$needs_system_deps" = "true" ]; then
command=(install-deps "${browsers[@]}")
deps_are_advisory=false
default_attempts=3
default_timeout=300
else
if [ "${PLAYWRIGHT_INSTALL_DEPS:-}" != "true" ]; then
echo "Browsers restored from the cache; skipping the apt font install."
exit 0
fi
# Preserve the opt-in Chromium refresh without stalling the hot path.
command=(install-deps "${browsers[@]}")
deps_are_advisory=true
default_attempts=1
default_timeout=120
fi
# Escape hatch: PLAYWRIGHT_INSTALL_DEPS=true puts apt back, bounded and
# advisory, for the day a runner image stops shipping one of the libraries.
command=(install-deps "${browsers[@]}")
deps_are_advisory=true
default_attempts=1
default_timeout=120
else
command=(install --with-deps "${browsers[@]}")
deps_are_advisory=false
Expand Down Expand Up @@ -98,18 +89,9 @@ for attempt in $(seq 1 "$attempts"); do
sleep 10
done

# With the binaries already restored from the cache, the only thing apt could
# still be adding is system libraries -- and the hosted runner image ships
# chromium's. Failing the job here means a stalled Ubuntu mirror is a red pull
# request, which is what actually happened: `noble-security InRelease` followed
# by four and a half minutes of total silence, three attempts in a row, twice on
# the same run. Sharding multiplied the number of jobs exposed to it.
#
# So on a cache hit the suite runs anyway. If a library really is missing,
# Playwright says so in as many words when it launches the browser, and the
# test step fails with that message instead of this one -- the information is
# not lost, it just no longer costs a run. A cold cache still has to succeed:
# there are no browsers to run without it.
# Only Chromium's explicit dependency refresh is advisory. Missing WebKit or
# Firefox libraries must fail here instead of producing hundreds of launch
# failures during the suite. A cold browser installation must also succeed.
if [ "$deps_are_advisory" = "true" ]; then
echo "::warning::playwright ${command[*]} failed after ${attempts} attempts; continuing on the cached browsers"
exit 0
Expand Down
37 changes: 25 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ permissions:
# (playwright.pages.config.ts explains why workerd dies under concurrent
# aborts). Shards keep every suite's own concurrency semantics untouched.
#
# Changing the shard count means editing both the matrix and the /N in the
# --shard argument of that job.
# Partitions use measured suite durations (bin/ci-shard-timings.json). Change
# the shard count in both the matrix and E2E_SHARD for that job.

jobs:
lint:
Expand Down Expand Up @@ -91,14 +91,18 @@ jobs:
# assert wall-clock budgets, and four WASM editors sharing four cores
# push them past their bound (the font-system wait measured 3.4 s
# against a 2 s budget). See the e2e-serial job and CLAUDE.md.
run: pnpm exec playwright test --shard=${{ matrix.shard }}/3 --grep-invert @serial
env:
E2E_SHARD: ${{ matrix.shard }}/3
run: pnpm exec playwright test --grep-invert @serial

- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v7
with:
name: playwright-report-${{ matrix.shard }}
path: playwright-report/
path: |
playwright-report/
test-results/
retention-days: 7

# Timing budgets need an idle VM, but do not need to run once per shard.
Expand All @@ -123,7 +127,9 @@ jobs:
uses: actions/upload-artifact@v7
with:
name: playwright-report-serial
path: playwright-report/
path: |
playwright-report/
test-results/
retention-days: 7

# Summary jobs. Branch protection matches required checks by name, so the
Expand Down Expand Up @@ -152,7 +158,7 @@ jobs:
e2e-pages-shard:
name: E2E (Cloudflare Pages semantics) shard ${{ matrix.shard }}
runs-on: ubuntu-latest
# Five shards: distribute individual tests while serving wrangler with a
# Five shards: balance measured suite durations while serving wrangler with a
# single worker on purpose (see playwright.pages.config.ts). Keep the
# runner count unchanged while improving the distribution; installing and
# starting a server in more jobs also adds overhead.
Expand All @@ -174,14 +180,18 @@ jobs:
- name: Run E2E against wrangler pages dev
# `@serial` is excluded by playwright.pages.config.ts itself, so a local
# run of that config measures exactly what this job does.
run: pnpm exec playwright test -c playwright.pages.config.ts --shard=${{ matrix.shard }}/5
env:
E2E_SHARD: ${{ matrix.shard }}/5
run: pnpm exec playwright test -c playwright.pages.config.ts

- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v7
with:
name: playwright-report-pages-${{ matrix.shard }}
path: playwright-report-pages/
path: |
playwright-report-pages/
test-results-pages/
retention-days: 7

e2e-pages:
Expand Down Expand Up @@ -223,18 +233,21 @@ jobs:
browsers: chromium

- name: Run E2E tests against the Docker image
# Called directly, not through `pnpm run`: pnpm swallows the extra
# arguments, which would silently run the whole suite in every shard.
# The explicit E2E_SHARD selects the duration-balanced partition.
# `@serial` is excluded by playwright.docker.config.ts itself, so a
# local `pnpm run test:e2e:docker` measures the same set.
run: sh ./bin/test-e2e-docker.sh --shard=${{ matrix.shard }}/3
env:
E2E_SHARD: ${{ matrix.shard }}/3
run: sh ./bin/test-e2e-docker.sh

- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v7
with:
name: playwright-report-docker-${{ matrix.shard }}
path: playwright-report-docker/
path: |
playwright-report-docker/
test-results-docker/
retention-days: 7

e2e-docker:
Expand Down
56 changes: 45 additions & 11 deletions .github/workflows/nightly-corpus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ on:
- cron: "17 19 * * *" # 03:17 Asia/Shanghai
workflow_dispatch:
inputs:
skip_corpus:
description: "Run cross-browser and budgets without the corpus matrix"
type: boolean
default: false
limit:
description: "Max files to run (CORPUS_LIMIT)"
default: "300"
Expand Down Expand Up @@ -52,6 +56,7 @@ jobs:
id: decide
env:
MANUAL: ${{ github.event_name == 'workflow_dispatch' }}
SKIP_CORPUS: ${{ github.event_name == 'workflow_dispatch' && inputs.skip_corpus }}
run: |
age_hours=$(( ( $(date -u +%s) - $(git log -1 --format=%ct) ) / 3600 ))
# 1..7 with 7 = Sunday. The cron fires at 19:17 UTC, so this is the
Expand All @@ -72,6 +77,10 @@ jobs:
if [ "$MANUAL" = "true" ] || { [ "$weekday" -eq 7 ] && [ "$age_hours" -lt 193 ]; }; then
corpus=true
fi
# A focused manual browser validation does not need the weekly corpus.
if [ "$SKIP_CORPUS" = "true" ]; then
corpus=false
fi

{
echo "HEAD is ${age_hours}h old, UTC weekday ${weekday}, manual=${MANUAL}"
Expand Down Expand Up @@ -134,7 +143,7 @@ jobs:
# are narrow on purpose: a bare `crash` would also drop
# 51921-Word-Crash067.doc, which is a real document from a bug report.
CORPUS_EXCLUDE: "password|protect|encrypt|corrupt|truncat|broken|invalid|damaged|clusterfuzz|fuzzer|fuzzed|poi-fuzz|crash-[0-9a-f]{6}|\\.xlsm$|\\.xlsb$|\\.docm$|\\.pptm$"
run: pnpm exec playwright test test/e2e/corpus.spec.ts --workers=2 --reporter=list
run: pnpm exec playwright test test/e2e/corpus.spec.ts --workers=2

- name: Summarize
if: always()
Expand All @@ -150,12 +159,16 @@ jobs:
playwright-report/
retention-days: 14

browsers:
name: Cross-browser (WebKit + Firefox)
browsers-shard:
name: Cross-browser (${{ matrix.browser }})
runs-on: ubuntu-latest
timeout-minutes: 90
needs: gate
if: needs.gate.outputs.browsers == 'true'
strategy:
fail-fast: false
matrix:
browser: [webkit, firefox]

steps:
- name: Checkout code
Expand All @@ -164,12 +177,15 @@ jobs:
- name: Set up the toolchain
uses: ./.github/actions/setup
with:
browsers: webkit firefox
browsers: ${{ matrix.browser }}

# The same PR-time suites (minus the opt-in sweeps) on the two engines
# the gate does not cover. Same-realm/L0 rules apply unchanged.
- name: Run E2E on WebKit and Firefox
run: pnpm exec playwright test -c playwright.browsers.config.ts --grep-invert "api surface|corpus|@serial" --reporter=list
- name: Run E2E on ${{ matrix.browser }}
env:
E2E_REPORT_DIR: playwright-report-browsers/normal
E2E_RESULTS_DIR: test-results-browsers/normal
run: pnpm exec playwright test -c playwright.browsers.config.ts --project=${{ matrix.browser }} --grep-invert "api surface|corpus|@serial" --workers=2

# The other half of that `--grep-invert`, exactly as ci.yml's e2e job
# pairs them: an @serial case is one that cannot share the run. Without
Expand All @@ -179,16 +195,34 @@ jobs:
# missing sentinel came from on 2026-08-25. Without the second pass
# nothing runs them at all and the job stays green regardless.
- name: Run the cases that cannot share the run
run: pnpm exec playwright test -c playwright.browsers.config.ts --grep @serial --workers=1 --reporter=list
if: ${{ !cancelled() }}
env:
E2E_REPORT_DIR: playwright-report-browsers/serial
E2E_RESULTS_DIR: test-results-browsers/serial
run: pnpm exec playwright test -c playwright.browsers.config.ts --project=${{ matrix.browser }} --grep @serial --workers=1

- name: Upload report
if: always()
uses: actions/upload-artifact@v7
with:
name: playwright-report-browsers
path: playwright-report-browsers/
name: playwright-report-browsers-${{ matrix.browser }}
path: |
playwright-report-browsers/
test-results-browsers/
retention-days: 14

browsers:
name: Cross-browser (WebKit + Firefox)
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [gate, browsers-shard]
if: always() && needs.gate.outputs.browsers == 'true'
steps:
- name: Report the browser matrix verdict
env:
RESULT: ${{ needs.browsers-shard.result }}
run: test "$RESULT" = "success"

budgets:
name: Slow-network budgets
runs-on: ubuntu-latest
Expand Down Expand Up @@ -226,7 +260,7 @@ jobs:
- name: UI crawl
env:
UI_CRAWL: "1"
run: pnpm exec playwright test test/e2e/ui-crawl.spec.ts --reporter=list
run: pnpm exec playwright test test/e2e/ui-crawl.spec.ts

- name: Upload UI crawl reports
if: always()
Expand All @@ -241,4 +275,4 @@ jobs:
- name: Throttled-network budget
env:
SLOW_NET: "1"
run: pnpm exec playwright test test/e2e/slow-network.spec.ts --reporter=list
run: pnpm exec playwright test test/e2e/slow-network.spec.ts
6 changes: 6 additions & 0 deletions bin/ci-artifacts.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { PlaywrightTestConfig } from '@playwright/test';
export function playwrightArtifacts(
report: string,
results: string,
env?: Record<string, string | undefined>,
): Pick<PlaywrightTestConfig, 'outputDir' | 'reporter'>;
7 changes: 7 additions & 0 deletions bin/ci-artifacts.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** Separate passes must never clear each other's HTML reports or trace output. */
export function playwrightArtifacts(report, results, env = process.env) {
return {
outputDir: env.E2E_RESULTS_DIR || results,
reporter: [['list'], ['html', { outputFolder: env.E2E_REPORT_DIR || report, open: 'never' }]],
};
}
Loading
Loading