fix: report broken worktree source symlinks (#3396)

This commit is contained in:
Vyctor H. Brzezowski
2026-08-05 14:28:41 -03:00
committed by GitHub
parent dc9da89d3b
commit fc0a47f02d
2 changed files with 100 additions and 1 deletions
+66
View File
@@ -6,6 +6,7 @@ import {
mkdirSync,
realpathSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
@@ -344,6 +345,71 @@ describe("setup-worktree", () => {
}
});
it("reports broken local-state symlinks in rejected worktree sources", () => {
withSourceAndCurrent((source, current) => {
symlinkSync("missing.env.local", join(source, ".env.local"));
symlinkSync("missing.convex", join(source, ".convex"));
expect(() =>
findSource(
{
force: false,
from: source,
quiet: true,
},
current,
),
).toThrow(
`Rejected sources:\n- ${source}: .env.local is a broken symlink to missing.env.local; .convex is a broken symlink to missing.convex`,
);
});
});
it("reports a broken Convex symlink when the source env remains readable", () => {
withSourceAndCurrent((source, current) => {
writeWorktree(source, "local-clawhub");
rmSync(join(source, ".convex"), { force: true, recursive: true });
symlinkSync("missing.convex", join(source, ".convex"));
expect(() =>
findSource(
{
force: false,
from: source,
quiet: true,
},
current,
),
).toThrow(`${source}: .convex is a broken symlink to missing.convex`);
});
});
it("accepts a remote source when only its unused Convex symlink is broken", () => {
withSourceAndCurrent((source, current) => {
writeWorktree(source, "remote-clawhub", {
env: {
CONVEX_DEPLOYMENT: "dev:remote-clawhub",
CONVEX_SITE_URL: null,
VITE_CONVEX_SITE_URL: null,
VITE_CONVEX_URL: "https://remote-clawhub.convex.cloud",
},
});
rmSync(join(source, ".convex"), { force: true, recursive: true });
symlinkSync("missing.convex", join(source, ".convex"));
expect(
findSource(
{
force: false,
from: source,
quiet: true,
},
current,
).path,
).toBe(source);
});
});
it("rejects local sources that point browser HTTP routes at the Convex function port", () => {
withSourceAndCurrent((source, current) => {
writeWorktree(source, "local-clawhub", {
+34 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bun
import { spawnSync } from "node:child_process";
import { existsSync, lstatSync, readFileSync, rmSync, symlinkSync } from "node:fs";
import { existsSync, lstatSync, readFileSync, readlinkSync, rmSync, symlinkSync } from "node:fs";
import { basename, resolve } from "node:path";
type Options = {
@@ -104,6 +104,25 @@ function readSource(path: string): Source | null {
};
}
function describeUnavailableSource(path: string, names: readonly (".env.local" | ".convex")[]) {
const problems: string[] = [];
for (const name of names) {
const localPath = resolve(path, name);
if (existsSync(localPath)) continue;
try {
if (lstatSync(localPath).isSymbolicLink()) {
problems.push(`${name} is a broken symlink to ${readlinkSync(localPath)}`);
}
} catch {
// Missing local state is normal; dangling links require operator repair.
}
}
return problems.join("; ");
}
function readConvexConfig(convexPath: string) {
const configPath = resolve(convexPath, "local/default/config.json");
return existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : null;
@@ -200,8 +219,22 @@ export function findSource(options: Options, cwd = process.cwd()) {
const rejected: string[] = [];
for (const candidate of candidates) {
const unavailableEnv = describeUnavailableSource(candidate, [".env.local"]);
if (unavailableEnv) {
const unavailableConvex = describeUnavailableSource(candidate, [".convex"]);
const unavailable = [unavailableEnv, unavailableConvex].filter(Boolean).join("; ");
rejected.push(`${candidate}: ${unavailable}`);
continue;
}
const source = readSource(candidate);
if (!source) continue;
const unavailableConvex = source.env.CONVEX_DEPLOYMENT?.startsWith("local:")
? describeUnavailableSource(candidate, [".convex"])
: "";
if (unavailableConvex) {
rejected.push(`${candidate}: ${unavailableConvex}`);
continue;
}
const invalid = validateSource(source);
if (!invalid) return source;
rejected.push(`${candidate}: ${invalid}`);