Copilot review round 6 — five findings (two inline, three suppressed), all valid. **A stale lock that cannot be cleared caused unlocked writes.** `acquireLock` does not only *create* directories: on finding a stale one it removes it and retries, and that removal can fail — `ENOTEMPTY` for a lock with anything inside it, `EACCES`/`EROFS` for one we may not touch. Those surface as ordinary non-`ELOCKED` errors, which the probe read as "locks do not work here" and degraded on. So every Inspector on the box quietly bypassed the *same* stuck lock and raced its writes — while the release-failure message was telling the operator saves would keep failing until they cleared it. `isStuckOrHeld` now discriminates on **whether the lock directory exists** rather than on an errno taxonomy: if it is there, something holds it and we refuse; if it is not, we genuinely could not create one and degrading is the documented trade (#1848, #1905). Reading the state the decision is about avoids enumerating error codes per platform — and an enumeration is exactly what let `ENOTEMPTY` through. **Identity was captured after the acquire promise settled.** That span includes the library's own `utimes`/`stat` probe, so a waiter replacing our directory inside it would have us record *the winner's* identity as our own, after which the removal guard would accept and delete their lock. It is now recorded in the `mkdir` callback — the moment the directory becomes ours, and synchronous with respect to this process, so there is no window. **The expiry advice was still wrong for `EACCES`/`EPERM`/`EROFS`.** Since the guard `stat`s before removing, everything reaching the release catch is an `rmdir` that was refused — and stale takeover reclaims through that same `rmdir`. The `ENOENT` arm was unreachable for the same reason, so the branch was dead code: removed, and the advice is now unconditional. **`proper-lockfile` was missing from `NODE_ONLY_OPTIMIZE_DEPS_EXCLUDE`.** A fourth externalization surface — the tsup lists configure the production bundles, not `vite dev`, whose dep scanner would otherwise walk its CJS/`graceful-fs`/`signal-exit` graph. Added, and `vite-base-config.test.ts` now asserts it along with `chokidar` and `@napi-rs/keyring`, which were also absent from that assertion. **A test was named for something it did not exercise.** "does not treat an unlistable directory as an empty one" was driving `ENOTDIR`, which is deliberately treated *as* the fresh-install case, so the actual new behaviour was untested. Split in two: one for `ENOTDIR`, and a real `EACCES` one using a `--x` directory — `readdir` denied, a known name still reachable, which is the asymmetry that makes "unlistable" different from "empty". Driven with a real mode rather than a stub, since `vi.spyOn` cannot redefine an ESM namespace export; skipped as root and on Windows, where the mode would not bite and the test would assert nothing. Both new tests verified by mutation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTHVxSu8AUgHRLvo1ntZ8H Signed-off-by: cliffhall <cliff@futurescale.com>
80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
import {
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
writeFileSync,
|
|
existsSync,
|
|
rmSync,
|
|
} from "node:fs";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import {
|
|
clearViteDepsCache,
|
|
getViteBaseConfig,
|
|
getViteDevOptimizeDeps,
|
|
} from "../../../../server/vite-base-config.js";
|
|
|
|
describe("getViteBaseConfig", () => {
|
|
it("excludes node-only deps from optimizeDeps so vite dev doesn't scan them", () => {
|
|
const config = getViteBaseConfig();
|
|
expect(config.optimizeDeps.exclude).toEqual(
|
|
expect.arrayContaining([
|
|
"@modelcontextprotocol/client/stdio",
|
|
"atomically",
|
|
"chokidar",
|
|
"cross-spawn",
|
|
"which",
|
|
"@napi-rs/keyring",
|
|
// #2082 — reached through `core/auth/node/file-lock.ts`. The tsup
|
|
// `external` lists cover the production bundles, not `vite dev`, so
|
|
// a node-only dependency has to be named in both places.
|
|
"proper-lockfile",
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("returns a fresh object each call (callers can mutate safely)", () => {
|
|
const a = getViteBaseConfig();
|
|
const b = getViteBaseConfig();
|
|
expect(a).not.toBe(b);
|
|
expect(a.optimizeDeps).not.toBe(b.optimizeDeps);
|
|
});
|
|
});
|
|
|
|
describe("getViteDevOptimizeDeps", () => {
|
|
it("forces a full pre-bundle on each dev launch with no stale-request 504s", () => {
|
|
const config = getViteDevOptimizeDeps();
|
|
expect(config.force).toBe(true);
|
|
expect(config.ignoreOutdatedRequests).toBe(true);
|
|
expect(config.include).toEqual([
|
|
"ajv",
|
|
"@modelcontextprotocol/client/validators/ajv",
|
|
]);
|
|
expect(config.exclude).toEqual(getViteBaseConfig().optimizeDeps.exclude);
|
|
});
|
|
});
|
|
|
|
describe("clearViteDepsCache", () => {
|
|
let tempRoot: string;
|
|
|
|
beforeEach(() => {
|
|
tempRoot = mkdtempSync(join(tmpdir(), "vite-cache-clear-"));
|
|
mkdirSync(join(tempRoot, "node_modules", ".vite", "deps"), {
|
|
recursive: true,
|
|
});
|
|
writeFileSync(
|
|
join(tempRoot, "node_modules", ".vite", "deps", "metadata.json"),
|
|
"{}",
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
it("removes node_modules/.vite before a dev server start", () => {
|
|
expect(existsSync(join(tempRoot, "node_modules", ".vite"))).toBe(true);
|
|
clearViteDepsCache(tempRoot);
|
|
expect(existsSync(join(tempRoot, "node_modules", ".vite"))).toBe(false);
|
|
});
|
|
});
|