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
60 changes: 60 additions & 0 deletions QuickShell.Raycast/src/__tests__/discovered-workspace-seed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { createWorkspaceFromDiscoveredGitRepo } from "../lib/discovered-workspace-seed";

describe("createWorkspaceFromDiscoveredGitRepo", () => {
it("preserves commands, companion app, and metadata for a newly discovered repository", () => {
const workspace = createWorkspaceFromDiscoveredGitRepo({
directory: "D:\\Dev\\Trackdub_Workspace\\Trackdub",
name: "Trackdub",
remoteUrl: "https://github.com/trackdub/trackdub",
devServerUrl: "http://localhost:5173",
tasks: [
{ label: "Dev server", command: "npm run dev", taskType: "frontend" },
{ label: "API", command: "dotnet watch", taskType: "api" },
],
companionSeed: { path: "C:\\Apps\\Cursor.exe", arguments: "." },
});

expect(workspace.name).toBe("Trackdub");
expect(workspace.abbreviation).toBe("trackdub");
expect(workspace.repoUrl).toBe("https://github.com/trackdub/trackdub");
expect(workspace.devServerUrl).toBe("http://localhost:5173");
expect(workspace.launches.map((launch) => launch.command)).toEqual(["npm run dev", "dotnet watch"]);
expect(workspace.launches.map((launch) => launch.taskType)).toEqual(["frontend", "api"]);
expect(workspace.companionApps).toEqual([
expect.objectContaining({ path: "C:\\Apps\\Cursor.exe", arguments: ".", openOnLaunch: true }),
]);
});

it("keeps a usable blank launch row when no command suggestion is available", () => {
const workspace = createWorkspaceFromDiscoveredGitRepo({
directory: "D:\\Dev\\empty-repo",
name: "",
tasks: [],
});

expect(workspace.name).toBe("empty-repo");
expect(workspace.launches).toHaveLength(1);
expect(workspace.launches[0].command).toBeNull();
});

it("reads the origin remote when discovery did not populate one", () => {
const directory = mkdtempSync(path.join(os.tmpdir(), "quickshell-discovered-seed-"));
try {
mkdirSync(path.join(directory, ".git"));
writeFileSync(
path.join(directory, ".git", "config"),
'[remote "origin"]\n\turl = git@github.com:trackdub/trackdub.git\n',
);

const workspace = createWorkspaceFromDiscoveredGitRepo({ directory, name: "Trackdub", tasks: [] });

expect(workspace.repoUrl).toBe("https://github.com/trackdub/trackdub");
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
});
183 changes: 181 additions & 2 deletions QuickShell.Raycast/src/__tests__/git-repo-discovery.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import { describe, expect, it } from "vitest";
import { buildSearchRoots, listDefaultRootCandidates, searchRootsFromWorkspaces } from "../lib/git-repo-search-roots";
import { describe, expect, it, vi } from "vitest";
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import {
buildSearchRoots,
discoverGitReposAsync,
discoverGitReposForQueryAsync,
listDefaultRootCandidates,
searchRootsFromWorkspaces,
} from "../lib/git-repo-discovery";

vi.mock("@raycast/utils", () => ({
withCache: <T extends (...args: never[]) => unknown>(fn: T) => fn,
}));

/** Windows-path contracts must not depend on the host OS (macOS CI uses darwin). */
const win32Roots = { pathStyle: "win32" as const };
Expand Down Expand Up @@ -94,3 +108,168 @@ describe("buildSearchRoots", () => {
expect(roots.map((root) => root.toLowerCase())).not.toContain("c:\\users\\tonyt");
});
});

describe("discoverGitReposForQueryAsync", () => {
it("does not probe direct paths on unsupported platforms", async () => {
const root = mkdtempSync(path.join(os.tmpdir(), "quickshell-git-platform-"));
const repo = path.join(root, "repo");
const platform = vi.spyOn(process, "platform", "get").mockReturnValue("linux");
try {
mkdirSync(path.join(repo, ".git"), { recursive: true });

const repos = await discoverGitReposForQueryAsync(repo);

expect(repos).toEqual([]);
} finally {
platform.mockRestore();
rmSync(root, { recursive: true, force: true });
}
});

it("bypasses the normal result cap for typed name searches", async () => {
const root = mkdtempSync(path.join(os.tmpdir(), "quickshell-git-search-"));
const repoCount = 51;
try {
for (let index = 0; index < repoCount; index += 1) {
mkdirSync(path.join(root, `repo-${index.toString().padStart(2, "0")}`, ".git"), { recursive: true });
}

// Keep this under Vitest's default 5s budget on slow Windows CI hosts.
const repos = await discoverGitReposForQueryAsync("repo-", [], {
rootDirectories: [root],
concurrency: 4,
});

expect(repos).toHaveLength(repoCount);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

it("finds the containing repository directly from an absolute path", async () => {
const root = mkdtempSync(path.join(os.tmpdir(), "quickshell-git-path-"));
const repo = path.join(root, "Trackdub");
const nested = path.join(repo, "src", "features");
try {
mkdirSync(path.join(repo, ".git"), { recursive: true });
mkdirSync(nested, { recursive: true });

const repos = await discoverGitReposForQueryAsync(nested);

expect(repos).toEqual([{ directory: repo, name: "Trackdub", remoteUrl: null }]);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

it("stops a targeted scan when it is already cancelled", async () => {
const controller = new AbortController();
controller.abort();

const repos = await discoverGitReposForQueryAsync("repo", [], { signal: controller.signal });

expect(repos).toEqual([]);
});

it("stops a targeted scan mid-flight when cancelled", async () => {
const root = mkdtempSync(path.join(os.tmpdir(), "quickshell-git-abort-"));
const repoCount = 16;
const controller = new AbortController();
const originalStat = fs.stat.bind(fs);
let statCalls = 0;
const stat = vi.spyOn(fs, "stat").mockImplementation(async (candidate) => {
statCalls += 1;
if (statCalls === 4) {
// Abort asynchronously after the scan has already started work.
queueMicrotask(() => controller.abort());
}
return originalStat(candidate);
});

try {
for (let index = 0; index < repoCount; index += 1) {
mkdirSync(path.join(root, `repo-${index.toString().padStart(2, "0")}`, ".git"), { recursive: true });
}

const repos = await discoverGitReposForQueryAsync("repo-", [], {
rootDirectories: [root],
concurrency: 1,
signal: controller.signal,
});

expect(repos.length).toBeLessThan(repoCount);
} finally {
stat.mockRestore();
rmSync(root, { recursive: true, force: true });
}
});

it("bounds total visited repositories without restoring the result cap", async () => {
const root = mkdtempSync(path.join(os.tmpdir(), "quickshell-git-budget-"));
try {
for (let index = 0; index < 20; index += 1) {
mkdirSync(path.join(root, `repo-${index.toString().padStart(2, "0")}`, ".git"), { recursive: true });
}

const repos = await discoverGitReposForQueryAsync("repo-", [], {
rootDirectories: [root],
concurrency: 1,
maxVisited: 11,
});

expect(repos).toHaveLength(10);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});

describe("discoverGitReposAsync", () => {
it("ignores expected stat lookup failures", async () => {
const stat = vi.spyOn(fs, "stat").mockRejectedValue(Object.assign(new Error("missing"), { code: "ENOENT" }));
try {
await expect(discoverGitReposAsync([], { rootDirectories: ["missing"] })).resolves.toEqual([]);
} finally {
stat.mockRestore();
}
});

it("surfaces unexpected stat failures", async () => {
const failure = new Error("unexpected stat failure");
const stat = vi.spyOn(fs, "stat").mockRejectedValue(failure);
try {
await expect(discoverGitReposAsync([], { rootDirectories: ["broken"] })).rejects.toBe(failure);
} finally {
stat.mockRestore();
}
});

it("reserves scan capacity before concurrent stat calls", async () => {
const root = mkdtempSync(path.join(os.tmpdir(), "quickshell-git-concurrency-"));
const roots = Array.from({ length: 4 }, (_, index) => path.join(root, `root-${index}`));
roots.forEach((directory) => mkdirSync(directory));

const originalStat = fs.stat.bind(fs);
let activeStats = 0;
let maxActiveStats = 0;
const stat = vi.spyOn(fs, "stat").mockImplementation(async (candidate) => {
activeStats += 1;
maxActiveStats = Math.max(maxActiveStats, activeStats);
await new Promise((resolve) => setTimeout(resolve, 10));
try {
return await originalStat(candidate);
} finally {
activeStats -= 1;
}
});

try {
await discoverGitReposAsync([], { rootDirectories: roots, concurrency: 4, maxScanned: 1 });

expect(maxActiveStats).toBe(1);
} finally {
stat.mockRestore();
rmSync(root, { recursive: true, force: true });
}
});
});
Loading
Loading