mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-05 16:04:43 +00:00
Eliminate recurring cross-platform CI flakes (#4171)
* fix(ci): eliminate recent cross-platform flakes Synchronize tests on owned state instead of elapsed time or transient identities. Preserve file rename collision safety when platform file IDs are unavailable. * fix(ci): close follow-up synchronization gaps Keep fetches in flight through buffered-event classification, scope mock agents to navigation tests, and wait for live workspace hydration before opening agent routes. * fix(ci): synchronize browser lifecycle boundaries
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
appendFile,
|
||||
chmod,
|
||||
link,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
@@ -547,10 +548,14 @@ describe("file explorer service", () => {
|
||||
try {
|
||||
await writeFile(path.join(root, "source.txt"), "source", "utf8");
|
||||
await writeFile(path.join(root, "existing.txt"), "existing", "utf8");
|
||||
await link(path.join(root, "source.txt"), path.join(root, "source-link.txt"));
|
||||
|
||||
await expect(
|
||||
renameExplorerEntry({ root, relativePath: "source.txt", name: "existing.txt" }),
|
||||
).resolves.toEqual({ status: "error", error: '"existing.txt" already exists' });
|
||||
await expect(
|
||||
renameExplorerEntry({ root, relativePath: "source.txt", name: "source-link.txt" }),
|
||||
).resolves.toEqual({ status: "error", error: '"source-link.txt" already exists' });
|
||||
await expect(
|
||||
renameExplorerEntry({ root, relativePath: "source.txt", name: "../outside.txt" }),
|
||||
).resolves.toEqual({ status: "error", error: "Name cannot contain path separators" });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { constants, promises as fs, type BigIntStats } from "fs";
|
||||
import { constants, promises as fs, type BigIntStats, type Stats } from "fs";
|
||||
import type { FileHandle } from "fs/promises";
|
||||
import path from "path";
|
||||
import { randomUUID } from "crypto";
|
||||
@@ -707,11 +707,10 @@ export async function renameExplorerEntry({
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
if (
|
||||
targetStats &&
|
||||
(targetStats.dev !== sourceStats.dev || targetStats.ino !== sourceStats.ino)
|
||||
) {
|
||||
return { status: "error", error: `"${trimmedName}" already exists` };
|
||||
if (targetStats) {
|
||||
if (!(await isCaseOnlyRename(source, targetPath, sourceStats, targetStats))) {
|
||||
return { status: "error", error: `"${trimmedName}" already exists` };
|
||||
}
|
||||
}
|
||||
|
||||
const sourcePath = normalizeRelativePath({ root, targetPath: source.requestedPath });
|
||||
@@ -782,6 +781,25 @@ function isEntryExistsError(error: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
async function isCaseOnlyRename(
|
||||
source: ScopedPath,
|
||||
targetPath: string,
|
||||
sourceStats: Stats,
|
||||
targetStats: Stats,
|
||||
): Promise<boolean> {
|
||||
const hasStableFileIds =
|
||||
sourceStats.dev !== 0 ||
|
||||
sourceStats.ino !== 0 ||
|
||||
targetStats.dev !== 0 ||
|
||||
targetStats.ino !== 0;
|
||||
const isSameEntry = hasStableFileIds
|
||||
? targetStats.dev === sourceStats.dev && targetStats.ino === sourceStats.ino
|
||||
: !sourceStats.isSymbolicLink() &&
|
||||
!targetStats.isSymbolicLink() &&
|
||||
(await fs.realpath(targetPath)) === source.resolvedPath;
|
||||
return isSameEntry && source.requestedPath.toLowerCase() === targetPath.toLowerCase();
|
||||
}
|
||||
|
||||
async function resolveScopedPath({
|
||||
root,
|
||||
relativePath = ".",
|
||||
|
||||
@@ -899,7 +899,9 @@ describe("WorkspaceGitService checkout observation", () => {
|
||||
{ path: path.join(GIT_DIR, "refs", "remotes", "origin", "main"), type: "update" },
|
||||
]);
|
||||
releaseFetch.resolve();
|
||||
await flushPromises();
|
||||
await vi.waitFor(() => {
|
||||
expect(service.getMetrics().fetchInFlightCount).toBe(0);
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
await vi.waitFor(() => {
|
||||
expect(getCheckoutSnapshotFacts).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -3141,26 +3141,36 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
||||
let result: WorkspaceGitFetchResult | null = null;
|
||||
const eventsBeforeFetchSnapshot: FileChange[] = [];
|
||||
try {
|
||||
result = await this.deps.runGitFetch(
|
||||
target.cwd,
|
||||
{
|
||||
onRefSnapshot: (phase) => {
|
||||
const events = target.bufferedFetchMetadataEvents.splice(0);
|
||||
if (phase === "before") {
|
||||
eventsBeforeFetchSnapshot.push(...events);
|
||||
}
|
||||
try {
|
||||
result = await this.deps.runGitFetch(
|
||||
target.cwd,
|
||||
{
|
||||
onRefSnapshot: (phase) => {
|
||||
const events = target.bufferedFetchMetadataEvents.splice(0);
|
||||
if (phase === "before") {
|
||||
eventsBeforeFetchSnapshot.push(...events);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
createRunGitCommand("background-fetch"),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
{ err: error, repoGitRoot: target.repoGitRoot, cwd: target.cwd },
|
||||
"Background git fetch failed",
|
||||
);
|
||||
createRunGitCommand("background-fetch"),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
{ err: error, repoGitRoot: target.repoGitRoot, cwd: target.cwd },
|
||||
"Background git fetch failed",
|
||||
);
|
||||
}
|
||||
this.applyRepoFetchResult(target, result, eventsBeforeFetchSnapshot);
|
||||
} finally {
|
||||
target.fetchInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
private applyRepoFetchResult(
|
||||
target: RepoGitTarget,
|
||||
result: WorkspaceGitFetchResult | null,
|
||||
eventsBeforeFetchSnapshot: FileChange[],
|
||||
): void {
|
||||
this.flushFetchMetadataEvents(target, eventsBeforeFetchSnapshot);
|
||||
if (!result || result.changes === null) {
|
||||
target.recentFetchRemoteRefChanges.clear();
|
||||
|
||||
Reference in New Issue
Block a user