Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/review/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"@hono/node-server": "2.0.12",
"@img/sharp-wasm32": "0.35.5",
"@modelcontextprotocol/sdk": "1.31.0",
"@parcel/watcher": "catalog:",
"commander": "14.0.3",
"fuzzysort": "4.0.2",
"hono": "4.13.7",
Expand Down
96 changes: 74 additions & 22 deletions packages/review/src/review-api/local-data.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { execFileSync, spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import {
type FSWatcher,
appendFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
readlinkSync,
realpathSync,
renameSync,
rmSync,
symlinkSync,
watch,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
Expand All @@ -24,6 +24,7 @@ import {
REVIEW_CLIENT_HEADER,
REVIEW_CLIENT_REMOTE,
} from "@dev.fast/review-protocol";
import { type SubscribeCallback, subscribe } from "@parcel/watcher";
import { rangeAnchor } from "@review/lens-selection";
import { createGlobalReviewServer } from "@review/server/desktop-server.js";
import { Hono } from "hono";
Expand Down Expand Up @@ -60,6 +61,9 @@ const git = (...args: string[]) =>

const spawns: string[][] = [];

/** Parcel delivers events late, in batches. */
const watcherLatency = () => new Promise((resolve) => setTimeout(resolve, 200));

const recordSpawns = () => {
spawns.length = 0;
setLocalVcsCommandObserver({
Expand Down Expand Up @@ -2658,7 +2662,7 @@ it("leaves other checkouts alone while a nested worktree works", async () => {
writeFileSync(path.join(sibling, "node_modules/built.js"), "built\n");
writeFileSync(path.join(sibling, source.file), "export const edited = 1;\n");
execFileSync("git", ["add", source.file], { cwd: sibling });
await new Promise((resolve) => setTimeout(resolve, 100));
await watcherLatency();
await local.store.refreshWorktrees();

expect(local.store.read(siblingReview).pins!.worktreeRevision).not.toBe(
Expand All @@ -2671,6 +2675,58 @@ it("leaves other checkouts alone while a nested worktree works", async () => {
).toEqual([]);
});

const inotifyWatches = () =>
readdirSync("/proc/self/fd").reduce((count, fd) => {
try {
if (readlinkSync(`/proc/self/fd/${fd}`) !== "anon_inode:inotify")
return count;

return (
count +
readFileSync(`/proc/self/fdinfo/${fd}`, "utf8")
.split("\n")
.filter((line) => line.startsWith("inotify wd")).length
);
} catch {
return count;
}
}, 0);

it.runIf(process.platform === "linux")(
"watches only tracked directories on Linux",
async () => {
const tree = (root: string) => {
for (let i = 0; i < 200; i++)
mkdirSync(path.join(repository, root, `${i}`, "nested"), {
recursive: true,
});
};

writeFileSync(
path.join(repository, ".gitignore"),
"node_modules/\ntarget/\n",
);
tree("node_modules");
tree(".git/dev-fast/reviews/other");
const before = inotifyWatches();

await local.store.execute(
command({
type: "create",
title: "Main",
target: { kind: "worktree", repositoryId: pins.repositoryId },
}),
);
await local.store.refreshWorktrees();
expect(inotifyWatches() - before).toBeLessThan(50);

tree("target");
await vi.waitFor(() => expect(inotifyWatches() - before).toBeLessThan(50), {
timeout: 5000,
});
},
);

it("follows its base branch when only the ref moves", async () => {
const linked = path.join(directory, "linked");
git("worktree", "add", "-q", "-b", "linked", linked);
Expand All @@ -2691,14 +2747,14 @@ it("follows its base branch when only the ref moves", async () => {
writeFileSync(path.join(linked, "linked.ts"), "export {};\n");
linkedGit("add", ".");
linkedGit("-c", "commit.gpgsign=false", "commit", "-qm", "Linked");
await new Promise((resolve) => setTimeout(resolve, 100));
await watcherLatency();
await local.store.refreshWorktrees();
const head = linkedGit("rev-parse", "HEAD");
expect(local.store.read(reviewId).pins!.base).not.toBe(head);

// Nothing in the linked checkout changes: only main moves.
git("update-ref", "refs/heads/main", head);
await new Promise((resolve) => setTimeout(resolve, 100));
await watcherLatency();
await local.store.refreshWorktrees();

expect(local.store.read(reviewId).pins!.base).toBe(head);
Expand Down Expand Up @@ -2743,7 +2799,7 @@ it("reads current working source across authored versions, commits and retargeti
path.join(repository, "example.ts"),
"export const live = 2;\n",
);
await new Promise((resolve) => setTimeout(resolve, 50));
await watcherLatency();
await local.store.refreshWorktrees();
const current = local.store.read(result.reviewId);
expect(
Expand All @@ -2762,7 +2818,7 @@ it("reads current working source across authored versions, commits and retargeti
expect(git("diff", "--cached")).toBe(beforeIndex);
git("add", ".");
git("commit", "-qm", "Save changes");
await new Promise((resolve) => setTimeout(resolve, 50));
await watcherLatency();
await local.store.refreshWorktrees();
expect(
await local.data.changes(local.store.read(result.reviewId).pins!),
Expand Down Expand Up @@ -2857,7 +2913,7 @@ describe("worktree base", () => {
const forkPoint = local.store.read(reviewId).pins!.base;

const refresh = async () => {
await new Promise((resolve) => setTimeout(resolve, 50));
await watcherLatency();
await local.store.refreshWorktrees();

return local.store.read(reviewId).pins!.base;
Expand Down Expand Up @@ -2950,7 +3006,7 @@ it("reads symlink text and an unborn repository without following external links
"-qm",
"First",
]);
await new Promise((resolve) => setTimeout(resolve, 50));
await watcherLatency();
await local.store.refreshWorktrees();
expect(local.store.read(result.reviewId).pins!.head).not.toBe(
snapshot.pins!.head,
Expand Down Expand Up @@ -2997,7 +3053,7 @@ it("keeps authored coordinates fixed as live source changes and warns only on un
path.join(repository, "range.ts"),
"// inserted\nconst first = 1;\nconst second = 2;\nconst third = 3;\n",
);
await new Promise((resolve) => setTimeout(resolve, 50));
await watcherLatency();
await local.store.refreshWorktrees();
expect(local.store.read(result.reviewId).document[0]).toMatchObject({
source: "head/range.ts#L2",
Expand Down Expand Up @@ -3334,7 +3390,7 @@ it("recovers persisted stale source and clears flags on set_target", async () =>
).rejects.toThrow("exceeds the pinned file");
writeFileSync(path.join(repository, "recover.ts"), original);
git("add", "-N", "recover.ts");
await new Promise((resolve) => setTimeout(resolve, 50));
await watcherLatency();
await local.store.refreshWorktrees();
expect(local.store.read(created.reviewId).staleSources).toEqual([]);
expect(local.store.read(created.reviewId).document[0]).toMatchObject({
Expand Down Expand Up @@ -3429,14 +3485,13 @@ it("does not report clean tracked symlinks and submodules as modified", async ()
it("continues capturing after watchers fail and stop emitting changes", async () => {
await local.data.close();
await local.store.close();
const watchers: FSWatcher[] = [];
const callbacks: SubscribeCallback[] = [];
local = openLocalReviewStore(database, {
watch: ((...args: Parameters<typeof watch>) => {
const watcher = watch(...args);
watchers.push(watcher);
subscribe: (dir, callback, options) => {
callbacks.push(callback);

return watcher;
}) as typeof watch,
return subscribe(dir, callback, options);
},
});

const created = await local.store.execute(
Expand All @@ -3451,11 +3506,8 @@ it("continues capturing after watchers fail and stop emitting changes", async ()
}),
);

for (const watcher of watchers)
watcher.emit(
"error",
Object.assign(new Error("Watch limit"), { code: "ENOSPC" }),
);
for (const callback of callbacks)
callback(new Error("No space left on device"), []);

for (const value of ["first change\n", "second change\n"]) {
writeFileSync(path.join(repository, "example.ts"), value);
Expand Down
Loading
Loading