From 1a019c111ceaa3989a8e5f075303b5000365e4f2 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:14:18 -0700 Subject: [PATCH] test(diff): repro for STA-3420 combined-diff invalidation freeze Co-authored-by: Orca --- ...ned-diff-invalidation-freeze-repro.spec.ts | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 tests/e2e/combined-diff-invalidation-freeze-repro.spec.ts diff --git a/tests/e2e/combined-diff-invalidation-freeze-repro.spec.ts b/tests/e2e/combined-diff-invalidation-freeze-repro.spec.ts new file mode 100644 index 00000000000..838e820a4b6 --- /dev/null +++ b/tests/e2e/combined-diff-invalidation-freeze-repro.spec.ts @@ -0,0 +1,164 @@ +import { execFileSync } from 'node:child_process' +import { rmSync } from 'node:fs' +import type { Page } from '@stablyai/playwright-test' +import { test, expect } from './helpers/orca-app' +import { waitForSessionReady } from './helpers/store' +import { createIsolatedStagedLocaleDiffRepo } from './large-diff-repro-fixtures' + +async function addAndActivateRepo(orcaPage: Page, repoPath: string): Promise { + const repoId = await orcaPage.evaluate(async (pathToRepo: string) => { + const store = window.__store + if (!store) { + throw new Error('window.__store is not available') + } + const addedRepo = await store.getState().addRepoPath(pathToRepo) + if (!addedRepo) { + throw new Error(`isolated repo not found: ${pathToRepo}`) + } + return addedRepo.id + }, repoPath) + + await expect + .poll( + () => + orcaPage.evaluate(async (targetRepoId: string) => { + const store = window.__store + if (!store) { + return 0 + } + await store.getState().fetchWorktrees(targetRepoId) + return store.getState().worktreesByRepo[targetRepoId]?.length ?? 0 + }, repoId), + { timeout: 30_000, message: 'isolated staged-diff worktree did not load' } + ) + .toBeGreaterThan(0) + + return orcaPage.evaluate( + ({ targetRepoId, pathToRepo }) => { + const store = window.__store + if (!store) { + throw new Error('window.__store is not available') + } + const state = store.getState() + const worktrees = state.worktreesByRepo[targetRepoId] ?? [] + const worktree = worktrees.find((entry) => entry.path === pathToRepo) ?? worktrees[0] + if (!worktree) { + throw new Error(`isolated worktree not found: ${pathToRepo}`) + } + state.setActiveRepo(targetRepoId) + state.setActiveWorktree(worktree.id) + return worktree.id + }, + { targetRepoId: repoId, pathToRepo: repoPath } + ) +} + +test.describe('Combined diff invalidation freeze repro (STA-3420)', () => { + test.describe.configure({ mode: 'serial' }) + test.use({ seedTestRepo: false }) + + test('committing under an open Staged Changes diff keeps the renderer responsive', async ({ + orcaPage + }) => { + await waitForSessionReady(orcaPage) + const fixture = createIsolatedStagedLocaleDiffRepo() + + try { + const worktreeId = await addAndActivateRepo(orcaPage, fixture.repoPath) + + const opened = await orcaPage.evaluate( + async ({ wId, repoPath }) => { + const store = window.__store + if (!store) { + throw new Error('window.__store is not available') + } + const status = await window.api.git.status({ worktreePath: repoPath }) + store.getState().setGitStatus(wId, status) + const staged = status.entries.filter((entry) => entry.area === 'staged') + if (staged.length === 0) { + throw new Error('fixture produced no staged entries') + } + // Why: mirrors the Source Control "Staged Changes" tab, which snapshots entries at open. + store.getState().openAllDiffs(wId, repoPath, undefined, 'staged', staged) + + const startedAt = performance.now() + let editorCount = 0 + while (performance.now() - startedAt < 30_000) { + await new Promise((resolve) => window.setTimeout(resolve, 50)) + editorCount = document.querySelectorAll('.monaco-diff-editor').length + if (editorCount > 0) { + await new Promise((resolve) => window.setTimeout(resolve, 1_500)) + editorCount = document.querySelectorAll('.monaco-diff-editor').length + break + } + } + return { stagedCount: staged.length, editorCount } + }, + { wId: worktreeId, repoPath: fixture.repoPath } + ) + console.log(`staged diff opened ${JSON.stringify(opened)}`) + expect(opened.editorCount).toBeGreaterThan(0) + + // Why: the reported freeze starts when the open diff is invalidated by a + // commit/rebase — the snapshot files stop having any staged diff at all. + execFileSync('git', ['commit', '-m', 'Invalidate the open staged diff'], { + cwd: fixture.repoPath, + stdio: 'pipe' + }) + + const measurement = await orcaPage.evaluate( + async ({ wId, repoPath }) => { + const store = window.__store + if (!store) { + throw new Error('window.__store is not available') + } + + const intervalMs = 50 + const samples: number[] = [] + let last = performance.now() + let maxLagMs = 0 + const timer = window.setInterval(() => { + const now = performance.now() + const lag = Math.max(0, now - last - intervalMs) + maxLagMs = Math.max(maxLagMs, lag) + samples.push(lag) + last = now + }, intervalMs) + + const startedAt = performance.now() + try { + // Why: the file watcher pushes several status refreshes while git + // rewrites the index; replay that churn instead of a single update. + for (let round = 0; round < 3; round += 1) { + const status = await window.api.git.status({ worktreePath: repoPath }) + store.getState().setGitStatus(wId, status) + await new Promise((resolve) => window.setTimeout(resolve, 700)) + } + await new Promise((resolve) => window.setTimeout(resolve, 3_000)) + } finally { + window.clearInterval(timer) + } + + const sorted = [...samples].sort((a, b) => a - b) + return { + elapsedMs: performance.now() - startedAt, + maxLagMs, + p95LagMs: sorted.length ? sorted[Math.floor(sorted.length * 0.95)] : 0, + sampleCount: samples.length, + editorCount: document.querySelectorAll('.monaco-diff-editor').length, + loadingRowCount: Array.from( + document.querySelectorAll('[data-combined-diff-section-row]') + ).filter((row) => row.textContent?.includes('Loading diff')).length, + sectionRowCount: document.querySelectorAll('[data-combined-diff-section-row]').length + } + }, + { wId: worktreeId, repoPath: fixture.repoPath } + ) + + console.log(`invalidation measurement ${JSON.stringify(measurement)}`) + expect(measurement.maxLagMs).toBeLessThan(1_000) + } finally { + rmSync(fixture.repoPath, { recursive: true, force: true }) + } + }) +})