From 7291eaa693fc5e1636ae35ed9e32dd742b49a677 Mon Sep 17 00:00:00 2001 From: yangshu Date: Fri, 17 Apr 2026 03:23:10 -0700 Subject: [PATCH 1/4] ci: optimize web playwright runtime and timing visibility --- .github/workflows/ci.yml | 93 +++++++++++++++++++++++++-- apps/web/e2e/ordinary-user-ci.spec.ts | 84 +++++++++++++++++------- docs/agent-handoff.md | 16 +++++ 3 files changed, 167 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba461d5..a5eb48d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ jobs: timeout-minutes: 15 env: PLAYWRIGHT_BROWSERS_PATH: /home/runner/.cache/ms-playwright + WEB_CI_STEP_TIMINGS_FILE: /tmp/web-ci-step-timings.tsv steps: - name: Checkout @@ -42,6 +43,13 @@ jobs: cache: pnpm cache-dependency-path: pnpm-lock.yaml + - name: Initialize CI step timing collector + run: | + : > "$WEB_CI_STEP_TIMINGS_FILE" + + - name: Mark Playwright cache restore start + run: echo "PLAYWRIGHT_CACHE_RESTORE_START=$(date +%s)" >> "$GITHUB_ENV" + - name: Restore Playwright Chromium cache id: playwright-cache uses: actions/cache/restore@v5 @@ -51,6 +59,15 @@ jobs: restore-keys: | playwright-chromium-${{ runner.os }}- + - name: Record Playwright cache restore duration + if: always() + env: + PLAYWRIGHT_CACHE_HIT: ${{ steps.playwright-cache.outputs.cache-hit }} + run: | + end_seconds=$(date +%s) + wall_seconds=$((end_seconds - PLAYWRIGHT_CACHE_RESTORE_START)) + printf "Restore Playwright Chromium cache\t%s\tcache-hit=%s\n" "$wall_seconds" "${PLAYWRIGHT_CACHE_HIT:-false}" >> "$WEB_CI_STEP_TIMINGS_FILE" + - name: Record CI runtime and cache inputs env: PLAYWRIGHT_CACHE_HIT: ${{ steps.playwright-cache.outputs.cache-hit }} @@ -67,13 +84,37 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: Install dependencies - run: pnpm install --frozen-lockfile + run: | + start_seconds=$(date +%s) + set +e + pnpm install --frozen-lockfile + status=$? + end_seconds=$(date +%s) + wall_seconds=$((end_seconds - start_seconds)) + printf "Install dependencies\t%s\texit=%s\n" "$wall_seconds" "$status" >> "$WEB_CI_STEP_TIMINGS_FILE" + exit "$status" - name: Install Playwright Chromium - run: pnpm --filter @draftorbit/web exec playwright install --with-deps chromium + run: | + start_seconds=$(date +%s) + set +e + pnpm --filter @draftorbit/web exec playwright install --with-deps chromium + status=$? + end_seconds=$(date +%s) + wall_seconds=$((end_seconds - start_seconds)) + printf "Install Playwright Chromium\t%s\texit=%s\n" "$wall_seconds" "$status" >> "$WEB_CI_STEP_TIMINGS_FILE" + exit "$status" - name: Web typecheck - run: pnpm --filter @draftorbit/web typecheck + run: | + start_seconds=$(date +%s) + set +e + pnpm --filter @draftorbit/web typecheck + status=$? + end_seconds=$(date +%s) + wall_seconds=$((end_seconds - start_seconds)) + printf "Web typecheck\t%s\texit=%s\n" "$wall_seconds" "$status" >> "$WEB_CI_STEP_TIMINGS_FILE" + exit "$status" - name: Web test (required) env: @@ -101,6 +142,7 @@ jobs: echo "| CI web test step wall time | ${wall_seconds}s |" echo "| exit code | ${status} |" } >> "$GITHUB_STEP_SUMMARY" + printf "Web test (required)\t%s\texit=%s\n" "$wall_seconds" "$status" >> "$WEB_CI_STEP_TIMINGS_FILE" exit "$status" - name: Web build @@ -108,7 +150,19 @@ jobs: NEXT_TELEMETRY_DISABLED: '1' NEXT_PUBLIC_API_URL: /__api NEXT_PUBLIC_ENABLE_LOCAL_LOGIN: 'true' - run: pnpm --filter @draftorbit/web build + run: | + start_seconds=$(date +%s) + set +e + pnpm --filter @draftorbit/web build + status=$? + end_seconds=$(date +%s) + wall_seconds=$((end_seconds - start_seconds)) + printf "Web build\t%s\texit=%s\n" "$wall_seconds" "$status" >> "$WEB_CI_STEP_TIMINGS_FILE" + exit "$status" + + - name: Mark Playwright cache save start + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: echo "PLAYWRIGHT_CACHE_SAVE_START=$(date +%s)" >> "$GITHUB_ENV" - name: Save Playwright Chromium cache if: steps.playwright-cache.outputs.cache-hit != 'true' @@ -117,6 +171,37 @@ jobs: path: ~/.cache/ms-playwright key: playwright-chromium-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} + - name: Record Playwright cache save duration + if: always() + env: + PLAYWRIGHT_CACHE_HIT: ${{ steps.playwright-cache.outputs.cache-hit }} + run: | + if [ "${PLAYWRIGHT_CACHE_HIT:-false}" = "true" ]; then + printf "Save Playwright Chromium cache\t0\tcache-hit=true (skipped)\n" >> "$WEB_CI_STEP_TIMINGS_FILE" + exit 0 + fi + end_seconds=$(date +%s) + wall_seconds=$((end_seconds - PLAYWRIGHT_CACHE_SAVE_START)) + printf "Save Playwright Chromium cache\t%s\texit=0\n" "$wall_seconds" >> "$WEB_CI_STEP_TIMINGS_FILE" + + - name: Publish CI step duration table + if: always() + run: | + { + echo "### CI step duration table" + echo + echo "| step | wall time (s) | note |" + echo "| --- | ---: | --- |" + if [ ! -s "$WEB_CI_STEP_TIMINGS_FILE" ]; then + echo "| none | 0 | timing file missing |" + else + while IFS=$'\t' read -r step wall_seconds note; do + [ -z "$step" ] && continue + echo "| ${step} | ${wall_seconds} | ${note} |" + done < "$WEB_CI_STEP_TIMINGS_FILE" + fi + } >> "$GITHUB_STEP_SUMMARY" + - name: Upload Playwright failure artifacts if: failure() uses: actions/upload-artifact@v7 diff --git a/apps/web/e2e/ordinary-user-ci.spec.ts b/apps/web/e2e/ordinary-user-ci.spec.ts index b9925a8..64163a0 100644 --- a/apps/web/e2e/ordinary-user-ci.spec.ts +++ b/apps/web/e2e/ordinary-user-ci.spec.ts @@ -457,9 +457,23 @@ async function openApp(page: Page) { await expect(page.getByText('未连接 X 账号 · 仍可先生成')).toBeVisible(); } -async function startGeneration(page: Page, input: { prompt: string; format?: 'tweet' | 'thread' | 'article'; visualMode?: string }) { - await openApp(page); +type GenerationScenario = { + name: string; + prompt: string; + format?: 'tweet' | 'thread' | 'article'; + visualMode?: string; + expected: RegExp[]; +}; + +async function ensureAdvancedOptionsOpen(page: Page) { + const visualModeSelect = page.locator('select[name="visualMode"]'); + if (await visualModeSelect.isVisible()) return; await page.getByText('高级选项').click(); + await expect(visualModeSelect).toBeVisible(); +} + +async function startGenerationInOpenApp(page: Page, input: { prompt: string; format?: 'tweet' | 'thread' | 'article'; visualMode?: string }) { + await ensureAdvancedOptionsOpen(page); if (input.format && input.format !== 'tweet') { const label = input.format === 'thread' ? '串推' : '长文'; await page.getByRole('button', { name: new RegExp(label, 'u') }).click(); @@ -472,6 +486,34 @@ async function startGeneration(page: Page, input: { prompt: string; format?: 'tw await expect(page.getByText('结果区')).toBeVisible(); } +async function startGeneration(page: Page, input: { prompt: string; format?: 'tweet' | 'thread' | 'article'; visualMode?: string }) { + await openApp(page); + await startGenerationInOpenApp(page, input); +} + +async function runGenerationScenario(page: Page, scenario: GenerationScenario) { + const scenarioStart = Date.now(); + await startGenerationInOpenApp(page, scenario); + for (const expected of scenario.expected) { + await expect(page.getByText(expected).first()).toBeVisible(); + } + + if (scenario.name.includes('thread')) { + await expect(page.getByRole('img', { name: /卡片组/u }).first()).toBeVisible(); + } + + if (scenario.name.includes('article')) { + await page.getByText('查看依据与配图建议').click(); + await expect(page.getByText('来源已抓取').first()).toBeVisible(); + } + + const bundleLink = page.getByRole('link', { name: /下载全部图文资产|下载 bundle/u }).first(); + await expect(bundleLink).toHaveAttribute('href', /token=/u); + await expect(page.getByRole('button', { name: /只重试图片\/图文资产/u })).toBeDisabled(); + const durationSeconds = ((Date.now() - scenarioStart) / 1000).toFixed(2); + console.log(`[ci-perf] generation scenario "${scenario.name}" completed in ${durationSeconds}s`); +} + test.beforeEach(async ({ page }) => { await mockDraftOrbitApi(page); }); @@ -496,7 +538,7 @@ test('ordinary user can enter the app from home local CTA with visible focus and await expect(page.getByText('未连接 X 账号 · 仍可先生成')).toBeVisible(); }); -const generationScenarios = [ +const generationScenariosFast: GenerationScenario[] = [ { name: 'tweet cover assets and safe publish gate', prompt: '别再靠灵感写推文,给我一条更像真人的冷启动判断句。', @@ -507,7 +549,10 @@ const generationScenarios = [ prompt: '把一个 AI 产品新功能写成 4 条 thread,不要像建议模板。', format: 'thread' as const, expected: [/1\/4/u, /4\/4/u, /下载 bundle/u] - }, + } +]; + +const generationScenariosRich: GenerationScenario[] = [ { name: 'article with cover infographic illustration and exports', prompt: '根据这篇来源写一篇关于最新 Hermes Agent 的 X 长文:https://example.com/source', @@ -517,31 +562,26 @@ const generationScenarios = [ { name: 'diagram visual mode', prompt: '用一条短推解释 DraftOrbit 从输入一句话到手动确认发布的 5 步流程,并配一个流程图:输入→来源→正文→图文→确认。', + format: 'tweet' as const, visualMode: 'diagram', expected: [/流程图/u, /输入→来源→正文→图文→确认/u, /下载 SVG/u] } ]; -test('app generation covers tweet thread article and diagram visual outputs', async ({ page }) => { - for (const scenario of generationScenarios) { +test('app generation covers tweet and thread visual outputs with minimal page churn', async ({ page }) => { + await openApp(page); + for (const scenario of generationScenariosFast) { await test.step(scenario.name, async () => { - await startGeneration(page, scenario); - for (const expected of scenario.expected) { - await expect(page.getByText(expected).first()).toBeVisible(); - } - - if (scenario.name.includes('thread')) { - await expect(page.getByRole('img', { name: /卡片组/u }).first()).toBeVisible(); - } - - if (scenario.name.includes('article')) { - await page.getByText('查看依据与配图建议').click(); - await expect(page.getByText('来源已抓取').first()).toBeVisible(); - } + await runGenerationScenario(page, scenario); + }); + } +}); - const bundleLink = page.getByRole('link', { name: /下载全部图文资产|下载 bundle/u }).first(); - await expect(bundleLink).toHaveAttribute('href', /token=/u); - await expect(page.getByRole('button', { name: /只重试图片\/图文资产/u })).toBeDisabled(); +test('app generation covers article and diagram visual outputs with minimal page churn', async ({ page }) => { + await openApp(page); + for (const scenario of generationScenariosRich) { + await test.step(scenario.name, async () => { + await runGenerationScenario(page, scenario); }); } }); diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index 53ddfdc..adeab62 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -4,6 +4,22 @@ Use this file to transfer execution state between Codex, Cursor, and other agent Update it before pausing work, switching tools, or asking another agent to continue. +## Current Playwright reporter-time stabilization pass (2026-04-17) + +- Worktree: `/Users/yangshu/.config/superpowers/worktrees/002-draftorbit.io/web-ci-perf-8s-stability` +- Branch: `codex/web-ci-perf-8s-stability` +- Base: `origin/main` at `90fb21816e7b3df9ce628fbcea390c699729f88f`. +- Goal: stabilize web Playwright reporter time around the 8s lane with lower run-to-run variance while preserving the required-check contract. +- Scenario optimization: + - `apps/web/e2e/ordinary-user-ci.spec.ts` now keeps the app open per generation-group test and reuses the same page/session across scenarios instead of reopening `/app` every scenario. + - Generation scenarios are split into two grouped tests (`tweet/thread` and `article/diagram`) to reduce per-step churn and improve 2-worker scheduling. + - Added per-scenario timing logs (`[ci-perf] generation scenario ...`) for direct hotspot inspection in Actions logs. +- CI observability upgrade: + - `.github/workflows/ci.yml` now writes a persistent `CI step duration table` into `$GITHUB_STEP_SUMMARY`. + - The table includes wall time + note for `Restore Playwright Chromium cache`, `Install Playwright Chromium`, `Web test (required)`, `Web build`, and cache save behavior. + - Added explicit restore/save timing rows (including skipped-on-cache-hit visibility) so long-tail cache behavior is observable across runs. + + ## Current main CI budget flake recovery (2026-04-17) - Worktree: `/Users/yangshu/.config/superpowers/worktrees/002-draftorbit.io/fix-main-web-budget-flake` From ec1c829927d9f1a00d41089ad0de0b05285fd983 Mon Sep 17 00:00:00 2001 From: yangshu Date: Fri, 17 Apr 2026 03:29:38 -0700 Subject: [PATCH 2/4] ci: tune web playwright workers for 8s reporter lane --- .github/workflows/ci.yml | 2 +- apps/web/e2e/ordinary-user-ci.spec.ts | 1 - docs/agent-handoff.md | 4 +++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5eb48d..73097d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,7 +123,7 @@ jobs: NEXT_PUBLIC_API_URL: /__api NEXT_PUBLIC_ENABLE_LOCAL_LOGIN: 'true' WEB_PLAYWRIGHT_PORT: 3300 - WEB_PLAYWRIGHT_WORKERS: '2' + WEB_PLAYWRIGHT_WORKERS: '3' WEB_PLAYWRIGHT_REPORTER_TARGET_SECONDS: '10' WEB_PLAYWRIGHT_REPORTER_HARD_BUDGET_SECONDS: '12' WEB_PLAYWRIGHT_ENFORCE_BUDGET: '1' diff --git a/apps/web/e2e/ordinary-user-ci.spec.ts b/apps/web/e2e/ordinary-user-ci.spec.ts index 64163a0..87f75ab 100644 --- a/apps/web/e2e/ordinary-user-ci.spec.ts +++ b/apps/web/e2e/ordinary-user-ci.spec.ts @@ -527,7 +527,6 @@ test('ordinary user can enter the app from home local CTA with visible focus and await localCta.hover(); await localCta.focus(); await expect(localCta).toBeFocused(); - await page.screenshot({ path: test.info().outputPath('home-local-cta-mobile.png'), fullPage: true }); const hasHorizontalOverflow = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth + 1); expect(hasHorizontalOverflow).toBe(false); diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index adeab62..fe33f0a 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -12,10 +12,12 @@ Update it before pausing work, switching tools, or asking another agent to conti - Goal: stabilize web Playwright reporter time around the 8s lane with lower run-to-run variance while preserving the required-check contract. - Scenario optimization: - `apps/web/e2e/ordinary-user-ci.spec.ts` now keeps the app open per generation-group test and reuses the same page/session across scenarios instead of reopening `/app` every scenario. - - Generation scenarios are split into two grouped tests (`tweet/thread` and `article/diagram`) to reduce per-step churn and improve 2-worker scheduling. + - Generation scenarios are split into two grouped tests (`tweet/thread` and `article/diagram`) to reduce per-step churn and improve CI worker scheduling. + - Mobile CTA test keeps hover/focus/overflow assertions but removes always-on screenshot capture in CI runs to cut avoidable I/O latency. - Added per-scenario timing logs (`[ci-perf] generation scenario ...`) for direct hotspot inspection in Actions logs. - CI observability upgrade: - `.github/workflows/ci.yml` now writes a persistent `CI step duration table` into `$GITHUB_STEP_SUMMARY`. + - Web Playwright workers are tuned to `3` in CI to improve parallel scheduling while staying below the prior contention seen at higher worker counts. - The table includes wall time + note for `Restore Playwright Chromium cache`, `Install Playwright Chromium`, `Web test (required)`, `Web build`, and cache save behavior. - Added explicit restore/save timing rows (including skipped-on-cache-hit visibility) so long-tail cache behavior is observable across runs. From 738e7fac6ab9da275de3f699d68673fabbb3b6a5 Mon Sep 17 00:00:00 2001 From: yangshu Date: Fri, 17 Apr 2026 03:34:04 -0700 Subject: [PATCH 3/4] test: reduce e2e page churn for fail-closed and retry flows --- apps/web/e2e/ordinary-user-ci.spec.ts | 15 ++++----------- docs/agent-handoff.md | 1 + 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/apps/web/e2e/ordinary-user-ci.spec.ts b/apps/web/e2e/ordinary-user-ci.spec.ts index 87f75ab..2f2ef72 100644 --- a/apps/web/e2e/ordinary-user-ci.spec.ts +++ b/apps/web/e2e/ordinary-user-ci.spec.ts @@ -486,11 +486,6 @@ async function startGenerationInOpenApp(page: Page, input: { prompt: string; for await expect(page.getByText('结果区')).toBeVisible(); } -async function startGeneration(page: Page, input: { prompt: string; format?: 'tweet' | 'thread' | 'article'; visualMode?: string }) { - await openApp(page); - await startGenerationInOpenApp(page, input); -} - async function runGenerationScenario(page: Page, scenario: GenerationScenario) { const scenarioStart = Date.now(); await startGenerationInOpenApp(page, scenario); @@ -585,8 +580,9 @@ test('app generation covers article and diagram visual outputs with minimal page } }); -test('app exposes Markdown copy success and retry-only visual asset recovery', async ({ page }) => { - await startGeneration(page, { prompt: '重试图文:生成一条带失败图片的短推,用来验证只重试图文资产。' }); +test('app handles retry-only visual recovery and latest-source fail-closed path in one user session', async ({ page }) => { + await openApp(page); + await startGenerationInOpenApp(page, { prompt: '重试图文:生成一条带失败图片的短推,用来验证只重试图文资产。' }); await expect(page.getByText('部分图片资产没有达到可发布标准')).toBeVisible(); const retryButton = page.getByRole('button', { name: /只重试图片\/图文资产/u }); @@ -597,10 +593,7 @@ test('app exposes Markdown copy success and retry-only visual asset recovery', a await page.getByRole('button', { name: '复制 Markdown' }).click(); await expect(page.getByText('Markdown 已复制')).toBeVisible(); -}); - -test('latest ambiguous source request fails closed with recoverable copy and no ready visual assets', async ({ page }) => { - await startGeneration(page, { prompt: '生成关于最新的 Hermes 的文章', format: 'article' }); + await startGenerationInOpenApp(page, { prompt: '生成关于最新的 Hermes 的文章', format: 'article' }); await expect(page.getByText('需要可靠来源,不能编造最新事实', { exact: true }).first()).toBeVisible(); await expect(page.getByRole('button', { name: '粘贴来源 URL 再生成' })).toBeVisible(); diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index fe33f0a..dadef0d 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -13,6 +13,7 @@ Update it before pausing work, switching tools, or asking another agent to conti - Scenario optimization: - `apps/web/e2e/ordinary-user-ci.spec.ts` now keeps the app open per generation-group test and reuses the same page/session across scenarios instead of reopening `/app` every scenario. - Generation scenarios are split into two grouped tests (`tweet/thread` and `article/diagram`) to reduce per-step churn and improve CI worker scheduling. + - Retry-only visual recovery + latest-source fail-closed assertions now run in one continuous app session test instead of two separate reopen flows. - Mobile CTA test keeps hover/focus/overflow assertions but removes always-on screenshot capture in CI runs to cut avoidable I/O latency. - Added per-scenario timing logs (`[ci-perf] generation scenario ...`) for direct hotspot inspection in Actions logs. - CI observability upgrade: From 6b90cbfd20ad67c3476f339f3934c746acb93630 Mon Sep 17 00:00:00 2001 From: yangshu Date: Fri, 17 Apr 2026 03:37:53 -0700 Subject: [PATCH 4/4] test: fold route-gate checks into home entry flow --- apps/web/e2e/ordinary-user-ci.spec.ts | 40 ++++++++++++--------------- docs/agent-handoff.md | 1 + 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/apps/web/e2e/ordinary-user-ci.spec.ts b/apps/web/e2e/ordinary-user-ci.spec.ts index 2f2ef72..e1d59e6 100644 --- a/apps/web/e2e/ordinary-user-ci.spec.ts +++ b/apps/web/e2e/ordinary-user-ci.spec.ts @@ -513,7 +513,7 @@ test.beforeEach(async ({ page }) => { await mockDraftOrbitApi(page); }); -test('ordinary user can enter the app from home local CTA with visible focus and responsive layout', async ({ page }) => { +test('ordinary user can enter the app from home local CTA and verify safe connect/queue/pricing gates', async ({ page }) => { await page.setViewportSize({ width: 375, height: 900 }); await page.goto('/'); @@ -530,6 +530,23 @@ test('ordinary user can enter the app from home local CTA with visible focus and await expect(page).toHaveURL(/\/app$/u); await expect(page.getByRole('button', { name: /开始生成/u })).toBeVisible(); await expect(page.getByText('未连接 X 账号 · 仍可先生成')).toBeVisible(); + + await page.goto('/connect?intent=connect_x_self'); + await expect(page).toHaveURL(/\/app\?nextAction=connect_x_self/u); + await expect(page.getByRole('heading', { name: '连接 X 账号后再发布会更顺' })).toBeVisible(); + await expect(page.getByRole('button', { name: /^连接 X 账号$/u })).toBeVisible(); + + await page.goto('/queue?intent=confirm_publish'); + await expect(page).toHaveURL(/\/app\?nextAction=confirm_publish/u); + await expect(page.getByText('确认这条内容是否发出')).toBeVisible(); + await expect(page.getByText('当前待确认内容')).toBeVisible(); + await expect(page.getByText('这条内容等待你确认后再发出')).toBeVisible(); + + await page.goto('/pricing'); + await expect(page.getByText('升级与结账')).toBeVisible(); + await expect(page.getByRole('button', { name: '月付' })).toBeVisible(); + await expect(page.getByRole('button', { name: /开始 3 天试用/u }).first()).toBeVisible(); + await expect(page).not.toHaveURL(/checkout\.example\.test/u); }); const generationScenariosFast: GenerationScenario[] = [ @@ -605,24 +622,3 @@ test('app handles retry-only visual recovery and latest-source fail-closed path await page.getByRole('button', { name: '粘贴来源 URL 再生成' }).click(); await expect(page.locator('textarea').first()).toHaveValue(/来源 URL:/u); }); - -test('connect queue and pricing routes expose safe manual gates without external posting or payment', async ({ page }) => { - await seedSession(page); - - await page.goto('/connect?intent=connect_x_self'); - await expect(page).toHaveURL(/\/app\?nextAction=connect_x_self/u); - await expect(page.getByRole('heading', { name: '连接 X 账号后再发布会更顺' })).toBeVisible(); - await expect(page.getByRole('button', { name: /^连接 X 账号$/u })).toBeVisible(); - - await page.goto('/queue?intent=confirm_publish'); - await expect(page).toHaveURL(/\/app\?nextAction=confirm_publish/u); - await expect(page.getByText('确认这条内容是否发出')).toBeVisible(); - await expect(page.getByText('当前待确认内容')).toBeVisible(); - await expect(page.getByText('这条内容等待你确认后再发出')).toBeVisible(); - - await page.goto('/pricing'); - await expect(page.getByText('升级与结账')).toBeVisible(); - await expect(page.getByRole('button', { name: '月付' })).toBeVisible(); - await expect(page.getByRole('button', { name: /开始 3 天试用/u }).first()).toBeVisible(); - await expect(page).not.toHaveURL(/checkout\.example\.test/u); -}); diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index dadef0d..7b07dd5 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -14,6 +14,7 @@ Update it before pausing work, switching tools, or asking another agent to conti - `apps/web/e2e/ordinary-user-ci.spec.ts` now keeps the app open per generation-group test and reuses the same page/session across scenarios instead of reopening `/app` every scenario. - Generation scenarios are split into two grouped tests (`tweet/thread` and `article/diagram`) to reduce per-step churn and improve CI worker scheduling. - Retry-only visual recovery + latest-source fail-closed assertions now run in one continuous app session test instead of two separate reopen flows. + - Connect/queue/pricing safe-gate checks are folded into the home→app entry test to remove an extra test lifecycle while keeping route coverage. - Mobile CTA test keeps hover/focus/overflow assertions but removes always-on screenshot capture in CI runs to cut avoidable I/O latency. - Added per-scenario timing logs (`[ci-perf] generation scenario ...`) for direct hotspot inspection in Actions logs. - CI observability upgrade: