Compare commits

...
Author SHA1 Message Date
paseo-bot[bot] fe7644f680 fix(server): measure degraded watchers through physical temp paths
Git canonicalizes the checkout root. Match that spelling in the benchmark fixture so a symlinked temporary directory still triggers the simulated watcher failure and degraded polling.
2026-10-19 00:54:56 +02:00
paseo-bot[bot] 3166730e36 fix(server): let Git observation measurements reach their summary
Startup discovery queues a refresh beyond the old 250 ms wait. Wait for Git command quiescence at phase boundaries so idle measurements exclude startup work, and pass the observer to healthy teardown before measuring degraded polling.
2026-10-19 00:50:05 +02:00
2 changed files with 68 additions and 6 deletions
@@ -1,5 +1,5 @@
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
@@ -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();
}
@@ -189,7 +191,11 @@ async function closeMeasuredService(input: {
}
async function main(): Promise<void> {
const tempDir = mkdtempSync(path.join(tmpdir(), "paseo-git-observation-measurement-"));
// Git reports physical roots; keep watcher-failure injection in that same
// spelling when the system temporary directory is a symlink.
const tempDir = realpathSync(
mkdtempSync(path.join(tmpdir(), "paseo-git-observation-measurement-")),
);
const repoDir = path.join(tempDir, "repo");
const paseoHome = path.join(tempDir, "paseo-home");
const trackedPath = path.join(repoDir, "tracked.txt");
@@ -243,7 +249,9 @@ async function main(): Promise<void> {
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 +350,7 @@ async function main(): Promise<void> {
diffManager = null;
await closeMeasuredService({
service: healthy.service,
observer: healthy.watcher.observer,
subscriptions: healthy.watcher.subscriptions,
});
healthy = null;
@@ -357,7 +366,9 @@ async function main(): Promise<void> {
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 = {
@@ -1,5 +1,15 @@
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import {
mkdirSync,
mkdtempSync,
readFileSync,
realpathSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
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 +532,44 @@ 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 root = realpathSync(mkdtempSync(path.join(tmpdir(), "paseo-measurement-test-")));
const actualTemp = path.join(root, "actual");
const aliasedTemp = path.join(root, "alias");
mkdirSync(actualTemp);
symlinkSync(actualTemp, aliasedTemp, "junction");
cleanup.push(async () => rmSync(root, { recursive: true, force: true }));
const { stdout } = await promisify(execFile)(process.execPath, ["--import", "tsx", script], {
env: { ...process.env, TMPDIR: aliasedTemp, TMP: aliasedTemp, TEMP: aliasedTemp },
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);