diff --git a/packages/server/scripts/measure-git-observation.ts b/packages/server/scripts/measure-git-observation.ts index b292c0e065..e2aa3281ff 100644 --- a/packages/server/scripts/measure-git-observation.ts +++ b/packages/server/scripts/measure-git-observation.ts @@ -12,6 +12,7 @@ import { import { startGitCommandMetrics, stopGitCommandMetrics, + waitForGitCommandMetricsIdle, type GitCommandMetricsSnapshot, } from "../src/utils/run-git-command.js"; import { CheckoutDiffManager } from "../src/server/checkout-diff-manager.js"; @@ -160,6 +161,7 @@ async function measurePhase(input: { serviceMetrics.workspaceRefreshQueuedCount === 0 ); }, "observation queue to drain"); + await waitForGitCommandMetricsIdle({ quietMs: 1_500, timeoutMs: 8_000 }); } finally { metrics = stopGitCommandMetrics(); } @@ -243,7 +245,9 @@ async function main(): Promise { healthy.watcher.subscriptions.size === 2, "healthy bootstrap", ); - await sleep(250); + // Linux startup discovery schedules a one-second debounced refresh even + // after subscriptions are ready. Keep that work in the bootstrap phase. + await waitForGitCommandMetricsIdle({ quietMs: 1_500, timeoutMs: 8_000 }); const bootstrapMetrics = stopGitCommandMetrics(); const bootstrapServiceMetrics = healthy.service.getMetrics(); phases.bootstrap = { @@ -342,6 +346,7 @@ async function main(): Promise { diffManager = null; await closeMeasuredService({ service: healthy.service, + observer: healthy.watcher.observer, subscriptions: healthy.watcher.subscriptions, }); healthy = null; @@ -357,7 +362,9 @@ async function main(): Promise { degraded.service.getMetrics().workspaceObservationSetupInFlightCount === 0, "degraded bootstrap", ); - await sleep(250); + // Linux startup discovery schedules a one-second debounced refresh even + // after subscriptions are ready. Keep that work in the bootstrap phase. + await waitForGitCommandMetricsIdle({ quietMs: 1_500, timeoutMs: 8_000 }); const degradedBootstrapMetrics = stopGitCommandMetrics(); const degradedBootstrapServiceMetrics = degraded.service.getMetrics(); phases.degradedBootstrap = { diff --git a/packages/server/src/server/workspace-git-service.observation.integration.test.ts b/packages/server/src/server/workspace-git-service.observation.integration.test.ts index 8ac1ef0018..b02289becb 100644 --- a/packages/server/src/server/workspace-git-service.observation.integration.test.ts +++ b/packages/server/src/server/workspace-git-service.observation.integration.test.ts @@ -1,5 +1,7 @@ import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; -import { execFileSync } from "node:child_process"; +import { execFile, execFileSync } from "node:child_process"; +import { promisify } from "node:util"; +import { fileURLToPath } from "node:url"; import { tmpdir } from "node:os"; import path from "node:path"; import pino from "pino"; @@ -522,3 +524,37 @@ test("a late Git-ignored tree is pruned while tracked changes still notify consu rmSync(tempDir, { recursive: true, force: true }); } }, 15_000); + +test("Git observation measurement separates startup from idle and reaches its degraded summary", async () => { + const script = fileURLToPath( + new URL("../../scripts/measure-git-observation.ts", import.meta.url), + ); + const { stdout } = await promisify(execFile)(process.execPath, ["--import", "tsx", script], { + timeout: 30_000, + maxBuffer: 1024 * 1024, + }); + const summary = stdout + .split("\n") + .find((line) => line.startsWith("GIT_OBSERVATION_MEASUREMENT ")); + expect(summary).toBeTypeOf("string"); + const report = JSON.parse(summary!.slice("GIT_OBSERVATION_MEASUREMENT ".length)); + expect(Object.keys(report.phases)).toEqual([ + "bootstrap", + "legacyFullRefreshReference", + "idle", + "activeDiff", + "editBurst", + "ignoredStorm", + "degradedBootstrap", + "degradedWatcher", + ]); + expect(report.phases.idle.gitCommands).toBe(0); + expect(report.phases.ignoredStorm.gitCommands).toBe(0); + expect(report.phases.bootstrap.producers.structural).toBeGreaterThanOrEqual(1); + expect(report.phases.activeDiff.producers.worktree).toBeGreaterThanOrEqual(1); + expect(report.phases.editBurst.producers.worktree).toBeLessThanOrEqual(2); + expect(report.phases.degradedWatcher.producers.worktree).toBeGreaterThanOrEqual(1); + for (const phase of Object.values(report.phases) as Array<{ queueAfter: unknown }>) { + expect(phase.queueAfter).toEqual({ inFlight: 0, queued: 0 }); + } +}, 35_000);