mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(migrate): validate workspace connection targets
This commit is contained in:
@@ -43,6 +43,21 @@ test("discovers the running daemon TCP address from its PID record", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("normalizes the daemon's bare IPv6 loopback PID address", () => {
|
||||
const paseoHome = mkdtempSync(path.join(os.tmpdir(), "paseo-node-client-pid-ipv6-"));
|
||||
cleanup.push(paseoHome);
|
||||
writeFileSync(path.join(paseoHome, "paseo.pid"), JSON.stringify({ listen: "::1:7789" }));
|
||||
|
||||
expect(resolveDefaultDaemonHosts({ PASEO_HOME: paseoHome })).toEqual([
|
||||
"[::1]:7789",
|
||||
"localhost:6767",
|
||||
]);
|
||||
expect(resolveDaemonTarget("::1:7789")).toEqual({
|
||||
type: "tcp",
|
||||
url: "ws://[::1]:7789/ws",
|
||||
});
|
||||
});
|
||||
|
||||
test("normalizes TCP, Unix, pipe, and Windows path-shaped targets", () => {
|
||||
expect(normalizeDaemonHost("tcp://Example.com:6767?ssl=true&password=secret")).toBe(
|
||||
"tcp://Example.com:6767?ssl=true&password=secret",
|
||||
|
||||
@@ -89,6 +89,9 @@ export function normalizeDaemonHost(raw: string): string | null {
|
||||
if (value.startsWith("/") || value.startsWith("~")) return `unix://${value}`;
|
||||
if (/^[A-Za-z]:[/\\]/.test(value)) return null;
|
||||
if (/^\d+$/.test(value)) return `127.0.0.1:${value}`;
|
||||
const ipv6Loopback = normalizeBareIpv6Loopback(value);
|
||||
if (ipv6Loopback) return ipv6Loopback;
|
||||
if (value.startsWith("::1:")) return null;
|
||||
return value.includes(":") ? value : null;
|
||||
}
|
||||
|
||||
@@ -166,7 +169,18 @@ export function resolveDaemonTarget(host: string): DaemonTarget {
|
||||
);
|
||||
return { type: "tcp", url: buildDaemonWebSocketUrl(endpoint, { useTls: parsed.useTls }) };
|
||||
}
|
||||
return { type: "tcp", url: `ws://${value}/ws` };
|
||||
const endpoint = normalizeBareIpv6Loopback(value) ?? normalizeHostPort(value);
|
||||
return { type: "tcp", url: buildDaemonWebSocketUrl(endpoint, { useTls: false }) };
|
||||
}
|
||||
|
||||
function normalizeBareIpv6Loopback(value: string): string | null {
|
||||
const match = value.match(/^::1:(\d{1,5})$/);
|
||||
if (!match) return null;
|
||||
try {
|
||||
return normalizeHostPort(`[::1]:${match[1]}`);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveDaemonPassword(
|
||||
|
||||
@@ -83,6 +83,41 @@ test("reports an unknown workspace state by its exact value", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("does not adopt an existing worktree on a different branch", () => {
|
||||
const repo = createRepository("wrong-worktree-branch", null);
|
||||
execFileSync("git", ["commit", "--allow-empty", "-m", "initial"], {
|
||||
cwd: repo,
|
||||
env: {
|
||||
...process.env,
|
||||
GIT_AUTHOR_NAME: "Paseo Test",
|
||||
GIT_AUTHOR_EMAIL: "test@paseo.local",
|
||||
GIT_COMMITTER_NAME: "Paseo Test",
|
||||
GIT_COMMITTER_EMAIL: "test@paseo.local",
|
||||
},
|
||||
});
|
||||
execFileSync("git", ["branch", "recorded-branch"], { cwd: repo });
|
||||
|
||||
const inspected = inspectCatalog({
|
||||
repos: [repoRecord("repo", repo)],
|
||||
workspaces: [
|
||||
{
|
||||
id: "workspace-wrong-branch",
|
||||
repoId: "repo",
|
||||
branch: "recorded-branch",
|
||||
state: "ready",
|
||||
path: repo,
|
||||
archiveCommit: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(inspected.projects[0]?.workspaces[0]).toMatchObject({
|
||||
sourceId: "workspace-wrong-branch",
|
||||
disposition: "invalid",
|
||||
notices: [{ code: "invalid-worktree" }],
|
||||
});
|
||||
});
|
||||
|
||||
function createRepository(name: string, settings: string | null): string {
|
||||
const repo = mkdtempSync(path.join(os.tmpdir(), `paseo-inspect-${name}-`));
|
||||
cleanup.push(repo);
|
||||
|
||||
@@ -112,7 +112,7 @@ function inspectWorkspace(
|
||||
}
|
||||
|
||||
if (workspace.path && isDirectory(workspace.path)) {
|
||||
const valid = isLinkedWorktree(repo.rootPath, workspace.path);
|
||||
const valid = isLinkedWorktree(repo.rootPath, workspace.path, workspace.branch);
|
||||
return {
|
||||
sourceId: workspace.id,
|
||||
state: "ready",
|
||||
@@ -126,7 +126,9 @@ function inspectWorkspace(
|
||||
: [
|
||||
notice(
|
||||
"invalid-worktree",
|
||||
`Skipped ${workspace.path}: not linked to ${repo.rootPath}.`,
|
||||
workspace.branch
|
||||
? `Skipped ${workspace.path}: not linked to ${repo.rootPath} on recorded branch ${workspace.branch}.`
|
||||
: `Skipped ${workspace.path}: not linked to ${repo.rootPath}.`,
|
||||
),
|
||||
],
|
||||
};
|
||||
@@ -174,14 +176,21 @@ function isGitRepository(rootPath: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function isLinkedWorktree(rootPath: string, workspacePath: string): boolean {
|
||||
function isLinkedWorktree(
|
||||
rootPath: string,
|
||||
workspacePath: string,
|
||||
expectedBranch: string | null,
|
||||
): boolean {
|
||||
try {
|
||||
const rootCommon = resolveGitPath(rootPath, git(rootPath, ["rev-parse", "--git-common-dir"]));
|
||||
const workspaceCommon = resolveGitPath(
|
||||
workspacePath,
|
||||
git(workspacePath, ["rev-parse", "--git-common-dir"]),
|
||||
);
|
||||
return realpathSync(rootCommon) === realpathSync(workspaceCommon);
|
||||
if (realpathSync(rootCommon) !== realpathSync(workspaceCommon)) return false;
|
||||
if (!expectedBranch) return true;
|
||||
const actualBranch = git(workspacePath, ["symbolic-ref", "--short", "HEAD"]);
|
||||
return actualBranch === expectedBranch.replace(/^refs\/heads\//, "");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user