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
167 changes: 167 additions & 0 deletions apps/server/src/git/worktreeCleanup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { describe, expect, it } from "vitest";

import { collectMergedWorktreeCleanupCandidates } from "./worktreeCleanup";

describe("collectMergedWorktreeCleanupCandidates", () => {
it("includes only merged pull request worktrees and ignores root, blank, and branchless entries", () => {
const candidates = collectMergedWorktreeCleanupCandidates({
cwd: "/repo",
worktreeListStdout: [
"worktree /repo/",
"branch refs/heads/feature-root",
"",
"worktree /repo/worktrees/feature-one",
"branch refs/heads/feature-one",
"",
"worktree /repo/worktrees/feature-two",
"branch refs/heads/feature-two",
"prunable gitdir file points to non-existent location",
"",
"worktree /repo/worktrees/no-branch",
"HEAD abcdef1234567890",
"",
"worktree ",
"branch refs/heads/feature-blank-path",
"",
].join("\n"),
mergedPullRequests: [
{
number: 101,
title: "Root PR",
url: "https://example.com/pr/101",
headBranch: "feature-root",
mergedAt: "2026-03-01T00:00:00.000Z",
},
{
number: 102,
title: "Feature one",
url: "https://example.com/pr/102",
headBranch: "feature-one",
mergedAt: "2026-03-02T00:00:00.000Z",
},
{
number: 103,
title: "Feature two",
url: "https://example.com/pr/103",
headBranch: "feature-two",
mergedAt: "2026-03-03T00:00:00.000Z",
},
{
number: 104,
title: "Blank path",
url: "https://example.com/pr/104",
headBranch: "feature-blank-path",
mergedAt: "2026-03-04T00:00:00.000Z",
},
],
});

expect(candidates).toEqual([
{
path: "/repo/worktrees/feature-one",
branch: "feature-one",
prNumber: 102,
prTitle: "Feature one",
prUrl: "https://example.com/pr/102",
mergedAt: "2026-03-02T00:00:00.000Z",
pathExists: true,
prunable: false,
},
{
path: "/repo/worktrees/feature-two",
branch: "feature-two",
prNumber: 103,
prTitle: "Feature two",
prUrl: "https://example.com/pr/103",
mergedAt: "2026-03-03T00:00:00.000Z",
pathExists: false,
prunable: true,
},
]);
});

it("marks prunable entries as missing paths", () => {
const [candidate] = collectMergedWorktreeCleanupCandidates({
cwd: "/repo",
worktreeListStdout: [
"worktree /repo/worktrees/feature-missing",
"branch refs/heads/feature-missing",
"prunable gitdir file points to non-existent location",
"",
].join("\n"),
mergedPullRequests: [
{
number: 201,
title: "Missing worktree",
url: "https://example.com/pr/201",
headBranch: "feature-missing",
mergedAt: "2026-03-01T00:00:00.000Z",
},
],
});

expect(candidate).toMatchObject({
branch: "feature-missing",
pathExists: false,
prunable: true,
});
});

it("sorts by existing paths first, then mergedAt descending, then branch name", () => {
const candidates = collectMergedWorktreeCleanupCandidates({
cwd: "/repo",
worktreeListStdout: [
"worktree /repo/worktrees/feature-beta",
"branch refs/heads/feature-beta",
"",
"worktree /repo/worktrees/feature-missing",
"branch refs/heads/feature-missing",
"prunable gitdir file points to non-existent location",
"",
"worktree /repo/worktrees/feature-recent",
"branch refs/heads/feature-recent",
"",
"worktree /repo/worktrees/feature-alpha",
"branch refs/heads/feature-alpha",
"",
].join("\n"),
mergedPullRequests: [
{
number: 301,
title: "Feature beta",
url: "https://example.com/pr/301",
headBranch: "feature-beta",
mergedAt: "2026-03-01T12:00:00.000Z",
},
{
number: 302,
title: "Feature missing",
url: "https://example.com/pr/302",
headBranch: "feature-missing",
mergedAt: "2026-04-01T12:00:00.000Z",
},
{
number: 303,
title: "Feature recent",
url: "https://example.com/pr/303",
headBranch: "feature-recent",
mergedAt: "2026-03-02T12:00:00.000Z",
},
{
number: 304,
title: "Feature alpha",
url: "https://example.com/pr/304",
headBranch: "feature-alpha",
mergedAt: "2026-03-01T12:00:00.000Z",
},
],
});

expect(candidates.map((candidate) => candidate.branch)).toEqual([
"feature-recent",
"feature-alpha",
"feature-beta",
"feature-missing",
]);
});
});
156 changes: 156 additions & 0 deletions apps/server/src/nativeFolderPicker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { afterEach, describe, expect, it, vi } from "vitest";

interface NativeFolderPickerModule {
pickFolderNative: () => string | null;
}

async function loadNativeFolderPicker(platformName: string) {
const spawnSyncMock = vi.fn();
const execFileSyncMock = vi.fn();

vi.resetModules();
vi.doMock("node:os", () => ({
platform: () => platformName,
}));
vi.doMock("node:child_process", () => ({
execFileSync: execFileSyncMock,
spawnSync: spawnSyncMock,
}));

const module = (await import("./nativeFolderPicker")) as NativeFolderPickerModule;

return {
execFileSyncMock,
pickFolderNative: module.pickFolderNative,
spawnSyncMock,
};
}

afterEach(() => {
vi.resetModules();
vi.restoreAllMocks();
});

describe("pickFolderNative", () => {
it("returns a trimmed macOS folder path from osascript", async () => {
const { pickFolderNative, spawnSyncMock } = await loadNativeFolderPicker("darwin");
spawnSyncMock.mockReturnValue({
error: undefined,
status: 0,
stdout: "/tmp/project/\n",
});

expect(pickFolderNative()).toBe("/tmp/project/");
expect(spawnSyncMock).toHaveBeenCalledWith(
"osascript",
["-e", 'POSIX path of (choose folder with prompt "Select project folder")'],
{ encoding: "utf8", timeout: 120_000 },
);
});

it("returns null when the macOS picker fails", async () => {
const { pickFolderNative, spawnSyncMock } = await loadNativeFolderPicker("darwin");
spawnSyncMock.mockReturnValue({
error: new Error("spawn failed"),
status: 1,
stdout: "",
});

expect(pickFolderNative()).toBeNull();
});

it("returns a trimmed Windows folder path from PowerShell", async () => {
const { execFileSyncMock, pickFolderNative } = await loadNativeFolderPicker("win32");
execFileSyncMock.mockReturnValue("C:\\Users\\okcode\\project\r\n");

expect(pickFolderNative()).toBe("C:\\Users\\okcode\\project");
expect(execFileSyncMock).toHaveBeenCalledWith(
"powershell.exe",
[
"-NoProfile",
"-NonInteractive",
"-Command",
"Add-Type -AssemblyName System.Windows.Forms; $d=New-Object System.Windows.Forms.FolderBrowserDialog; $d.Description='Select project folder'; if($d.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK){ $d.SelectedPath }",
],
{ encoding: "utf8", timeout: 120_000, windowsHide: true, maxBuffer: 4096 },
);
});

it("returns null when the Windows picker throws", async () => {
const { execFileSyncMock, pickFolderNative } = await loadNativeFolderPicker("win32");
execFileSyncMock.mockImplementation(() => {
throw new Error("powershell failed");
});

expect(pickFolderNative()).toBeNull();
});

it("prefers zenity on Linux when it succeeds", async () => {
const { pickFolderNative, spawnSyncMock } = await loadNativeFolderPicker("linux");
spawnSyncMock.mockReturnValueOnce({
error: undefined,
status: 0,
stdout: "/tmp/linux-project\n",
});

expect(pickFolderNative()).toBe("/tmp/linux-project");
expect(spawnSyncMock).toHaveBeenCalledTimes(1);
expect(spawnSyncMock).toHaveBeenCalledWith(
"zenity",
["--file-selection", "--directory", "--title=Select project folder"],
{
encoding: "utf8",
timeout: 120_000,
},
);
});

it("falls back to kdialog on Linux when zenity fails", async () => {
const { pickFolderNative, spawnSyncMock } = await loadNativeFolderPicker("linux");
spawnSyncMock
.mockReturnValueOnce({
error: new Error("zenity missing"),
status: 1,
stdout: "",
})
.mockReturnValueOnce({
error: undefined,
status: 0,
stdout: "/tmp/kdialog-project\n",
});

expect(pickFolderNative()).toBe("/tmp/kdialog-project");
expect(spawnSyncMock).toHaveBeenNthCalledWith(
2,
"kdialog",
["--getexistingdirectory", ".", "--title", "Select project folder"],
{ encoding: "utf8", timeout: 120_000 },
);
});

it("returns null on Linux when both pickers fail", async () => {
const { pickFolderNative, spawnSyncMock } = await loadNativeFolderPicker("linux");
spawnSyncMock
.mockReturnValueOnce({
error: new Error("zenity missing"),
status: 1,
stdout: "",
})
.mockReturnValueOnce({
error: new Error("kdialog missing"),
status: 1,
stdout: "",
});

expect(pickFolderNative()).toBeNull();
});

it("returns null on unsupported platforms without invoking child processes", async () => {
const { execFileSyncMock, pickFolderNative, spawnSyncMock } =
await loadNativeFolderPicker("freebsd");

expect(pickFolderNative()).toBeNull();
expect(spawnSyncMock).not.toHaveBeenCalled();
expect(execFileSyncMock).not.toHaveBeenCalled();
});
});
60 changes: 60 additions & 0 deletions apps/server/src/wsServer/readiness.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Effect, Fiber } from "effect";
import { describe, expect, it } from "vitest";

import { makeServerReadiness } from "./readiness";

describe("makeServerReadiness", () => {
it("stays pending until all readiness markers complete", async () => {
await Effect.runPromise(
Effect.gen(function* () {
const readiness = yield* makeServerReadiness;
const readyFiber = yield* readiness.awaitServerReady.pipe(Effect.forkScoped);

expect(readyFiber.pollUnsafe()).toBeUndefined();

yield* readiness.markHttpListening;
expect(readyFiber.pollUnsafe()).toBeUndefined();

yield* readiness.markPushBusReady;
expect(readyFiber.pollUnsafe()).toBeUndefined();

yield* readiness.markKeybindingsReady;
expect(readyFiber.pollUnsafe()).toBeUndefined();

yield* readiness.markTerminalSubscriptionsReady;
expect(readyFiber.pollUnsafe()).toBeUndefined();

yield* readiness.markOrchestrationSubscriptionsReady;
yield* Fiber.join(readyFiber);

expect(readyFiber.pollUnsafe()).not.toBeUndefined();
}).pipe(Effect.scoped),
);
});

it("resolves regardless of the order markers complete", async () => {
await Effect.runPromise(
Effect.gen(function* () {
const readiness = yield* makeServerReadiness;
const readyFiber = yield* readiness.awaitServerReady.pipe(Effect.forkScoped);

yield* readiness.markOrchestrationSubscriptionsReady;
expect(readyFiber.pollUnsafe()).toBeUndefined();

yield* readiness.markTerminalSubscriptionsReady;
expect(readyFiber.pollUnsafe()).toBeUndefined();

yield* readiness.markKeybindingsReady;
expect(readyFiber.pollUnsafe()).toBeUndefined();

yield* readiness.markPushBusReady;
expect(readyFiber.pollUnsafe()).toBeUndefined();

yield* readiness.markHttpListening;
yield* Fiber.join(readyFiber);

expect(readyFiber.pollUnsafe()).not.toBeUndefined();
}).pipe(Effect.scoped),
);
});
});
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@
"@types/babel__core": "^7.20.5",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/react-test-renderer": "^19.0.0",
"@vitejs/plugin-react": "^6.0.0",
"@vitest/browser-playwright": "^4.0.18",
"babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112",
"msw": "2.12.11",
"playwright": "^1.58.2",
"react-test-renderer": "^19.0.0",
"tailwindcss": "^4.0.0",
"typescript": "catalog:",
"vite": "^8.0.0",
Expand Down
Loading
Loading