mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-27 19:52:49 +00:00
* feat(app): add file manager context actions * fix: reshape file context actions --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
307 lines
9.5 KiB
TypeScript
307 lines
9.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
|
import { queryClient as appQueryClient } from "@/data/query-client";
|
|
import { useSessionStore } from "@/stores/session-store";
|
|
import {
|
|
__resetCheckoutGitActionsStoreForTests,
|
|
useCheckoutGitActionsStore,
|
|
} from "@/git/actions-store";
|
|
|
|
vi.mock("@react-native-async-storage/async-storage", () => ({
|
|
default: {
|
|
getItem: vi.fn(async () => null),
|
|
setItem: vi.fn(async () => undefined),
|
|
removeItem: vi.fn(async () => undefined),
|
|
},
|
|
}));
|
|
|
|
function createDeferred<T>() {
|
|
let resolve!: (value: T) => void;
|
|
let reject!: (reason?: unknown) => void;
|
|
const promise = new Promise<T>((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
return { promise, resolve, reject };
|
|
}
|
|
|
|
describe("checkout-git-actions-store", () => {
|
|
const serverId = "server-1";
|
|
const cwd = "/tmp/repo/worktrees/feature";
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
__resetCheckoutGitActionsStoreForTests();
|
|
appQueryClient.clear();
|
|
useSessionStore.setState((state) => ({ ...state, sessions: {} }));
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
__resetCheckoutGitActionsStoreForTests();
|
|
appQueryClient.clear();
|
|
useSessionStore.setState((state) => ({ ...state, sessions: {} }));
|
|
});
|
|
|
|
it("shares pending state per checkout and de-dupes in-flight calls", async () => {
|
|
const deferred = createDeferred<unknown>();
|
|
const client = {
|
|
checkoutCommit: vi.fn(() => deferred.promise),
|
|
};
|
|
|
|
useSessionStore.setState((state) => ({
|
|
...state,
|
|
sessions: {
|
|
...state.sessions,
|
|
[serverId]: { client } as unknown as (typeof state.sessions)[string],
|
|
},
|
|
}));
|
|
|
|
const store = useCheckoutGitActionsStore.getState();
|
|
|
|
const first = store.commit({ serverId, cwd });
|
|
const second = store.commit({ serverId, cwd });
|
|
|
|
expect(store.getStatus({ serverId, cwd, actionId: "commit" })).toBe("pending");
|
|
|
|
deferred.resolve({});
|
|
await Promise.all([first, second]);
|
|
|
|
expect(store.getStatus({ serverId, cwd, actionId: "commit" })).toBe("success");
|
|
|
|
vi.advanceTimersByTime(1000);
|
|
expect(store.getStatus({ serverId, cwd, actionId: "commit" })).toBe("idle");
|
|
});
|
|
|
|
it("runs pull then push sequentially for pull-and-push", async () => {
|
|
const order: string[] = [];
|
|
const client = {
|
|
checkoutPull: vi.fn(async () => {
|
|
order.push("pull");
|
|
return {};
|
|
}),
|
|
checkoutPush: vi.fn(async () => {
|
|
order.push("push");
|
|
return {};
|
|
}),
|
|
};
|
|
useSessionStore.setState((state) => ({
|
|
...state,
|
|
sessions: {
|
|
...state.sessions,
|
|
[serverId]: { client } as unknown as (typeof state.sessions)[string],
|
|
},
|
|
}));
|
|
|
|
await useCheckoutGitActionsStore.getState().pullAndPush({ serverId, cwd });
|
|
|
|
expect(order).toEqual(["pull", "push"]);
|
|
expect(
|
|
useCheckoutGitActionsStore.getState().getStatus({ serverId, cwd, actionId: "pull-and-push" }),
|
|
).toBe("success");
|
|
});
|
|
|
|
it("does not push when pull fails for pull-and-push", async () => {
|
|
const client = {
|
|
checkoutPull: vi.fn(async () => ({ error: { message: "pull conflict" } })),
|
|
checkoutPush: vi.fn(async () => ({})),
|
|
};
|
|
useSessionStore.setState((state) => ({
|
|
...state,
|
|
sessions: {
|
|
...state.sessions,
|
|
[serverId]: { client } as unknown as (typeof state.sessions)[string],
|
|
},
|
|
}));
|
|
|
|
await expect(
|
|
useCheckoutGitActionsStore.getState().pullAndPush({ serverId, cwd }),
|
|
).rejects.toThrow("pull conflict");
|
|
expect(
|
|
useCheckoutGitActionsStore.getState().getStatus({ serverId, cwd, actionId: "pull-and-push" }),
|
|
).toBe("idle");
|
|
});
|
|
|
|
it("surfaces push errors from pull-and-push after a successful pull", async () => {
|
|
const client = {
|
|
checkoutPull: vi.fn(async () => ({})),
|
|
checkoutPush: vi.fn(async () => ({ error: { message: "push rejected" } })),
|
|
};
|
|
useSessionStore.setState((state) => ({
|
|
...state,
|
|
sessions: {
|
|
...state.sessions,
|
|
[serverId]: { client } as unknown as (typeof state.sessions)[string],
|
|
},
|
|
}));
|
|
|
|
await expect(
|
|
useCheckoutGitActionsStore.getState().pullAndPush({ serverId, cwd }),
|
|
).rejects.toThrow("push rejected");
|
|
expect(
|
|
useCheckoutGitActionsStore.getState().getStatus({ serverId, cwd, actionId: "pull-and-push" }),
|
|
).toBe("idle");
|
|
});
|
|
|
|
it("refreshes git and GitHub state and reports success", async () => {
|
|
const client = {
|
|
checkoutRefresh: vi.fn(async () => ({ success: true, error: null })),
|
|
};
|
|
useSessionStore.setState((state) => ({
|
|
...state,
|
|
sessions: {
|
|
...state.sessions,
|
|
[serverId]: { client } as unknown as (typeof state.sessions)[string],
|
|
},
|
|
}));
|
|
|
|
await useCheckoutGitActionsStore.getState().refresh({ serverId, cwd });
|
|
|
|
expect(client.checkoutRefresh).toHaveBeenCalledWith(cwd);
|
|
expect(
|
|
useCheckoutGitActionsStore.getState().getStatus({ serverId, cwd, actionId: "refresh" }),
|
|
).toBe("success");
|
|
});
|
|
|
|
it("surfaces a refresh error and returns to idle", async () => {
|
|
const client = {
|
|
checkoutRefresh: vi.fn(async () => ({ error: { message: "not a git repository" } })),
|
|
};
|
|
useSessionStore.setState((state) => ({
|
|
...state,
|
|
sessions: {
|
|
...state.sessions,
|
|
[serverId]: { client } as unknown as (typeof state.sessions)[string],
|
|
},
|
|
}));
|
|
|
|
await expect(useCheckoutGitActionsStore.getState().refresh({ serverId, cwd })).rejects.toThrow(
|
|
"not a git repository",
|
|
);
|
|
expect(
|
|
useCheckoutGitActionsStore.getState().getStatus({ serverId, cwd, actionId: "refresh" }),
|
|
).toBe("idle");
|
|
});
|
|
|
|
it("discards selected paths through the shared checkout action workflow", async () => {
|
|
const checkoutDiscardChanges = vi.fn(async () => ({ success: true, error: null }));
|
|
const client = { checkoutDiscardChanges };
|
|
useSessionStore.setState((state) => ({
|
|
...state,
|
|
sessions: {
|
|
...state.sessions,
|
|
[serverId]: { client } as unknown as (typeof state.sessions)[string],
|
|
},
|
|
}));
|
|
|
|
await useCheckoutGitActionsStore
|
|
.getState()
|
|
.discardChanges({ serverId, cwd, paths: ["renamed.ts", "original.ts"] });
|
|
|
|
expect(checkoutDiscardChanges).toHaveBeenCalledWith(cwd, {
|
|
paths: ["renamed.ts", "original.ts"],
|
|
});
|
|
expect(
|
|
useCheckoutGitActionsStore
|
|
.getState()
|
|
.getStatus({ serverId, cwd, actionId: "discard-changes" }),
|
|
).toBe("success");
|
|
});
|
|
|
|
for (const rpc of [
|
|
{
|
|
label: "forge",
|
|
method: "checkoutForgeSetAutoMerge",
|
|
feature: "checkoutForgeSetAutoMerge",
|
|
},
|
|
{
|
|
label: "legacy GitHub",
|
|
method: "checkoutGithubSetAutoMerge",
|
|
feature: "checkoutGithubSetAutoMerge",
|
|
},
|
|
] as const) {
|
|
it(`enables PR auto-merge through the ${rpc.label} RPC`, async () => {
|
|
const setAutoMerge = vi.fn(async () => ({
|
|
enabled: true,
|
|
success: true,
|
|
error: null,
|
|
}));
|
|
const client = { [rpc.method]: setAutoMerge };
|
|
useSessionStore.getState().initializeSession(serverId, client as unknown as DaemonClient);
|
|
useSessionStore.getState().updateSessionServerInfo(serverId, {
|
|
serverId,
|
|
hostname: null,
|
|
version: null,
|
|
features: { [rpc.feature]: true },
|
|
});
|
|
|
|
await useCheckoutGitActionsStore
|
|
.getState()
|
|
.enablePrAutoMerge({ serverId, cwd, method: "squash" });
|
|
|
|
expect(setAutoMerge).toHaveBeenCalledWith(cwd, {
|
|
enabled: true,
|
|
method: "squash",
|
|
});
|
|
expect(
|
|
useCheckoutGitActionsStore
|
|
.getState()
|
|
.getStatus({ serverId, cwd, actionId: "enable-pr-auto-merge-squash" }),
|
|
).toBe("success");
|
|
});
|
|
|
|
it(`disables PR auto-merge through the ${rpc.label} RPC`, async () => {
|
|
const setAutoMerge = vi.fn(async () => ({
|
|
enabled: false,
|
|
success: true,
|
|
error: null,
|
|
}));
|
|
const client = { [rpc.method]: setAutoMerge };
|
|
useSessionStore.getState().initializeSession(serverId, client as unknown as DaemonClient);
|
|
useSessionStore.getState().updateSessionServerInfo(serverId, {
|
|
serverId,
|
|
hostname: null,
|
|
version: null,
|
|
features: { [rpc.feature]: true },
|
|
});
|
|
|
|
await useCheckoutGitActionsStore.getState().disablePrAutoMerge({ serverId, cwd });
|
|
|
|
expect(setAutoMerge).toHaveBeenCalledWith(cwd, { enabled: false });
|
|
expect(
|
|
useCheckoutGitActionsStore
|
|
.getState()
|
|
.getStatus({ serverId, cwd, actionId: "disable-pr-auto-merge" }),
|
|
).toBe("success");
|
|
});
|
|
}
|
|
|
|
it("does not call PR auto-merge RPCs when the daemon lacks the feature flag", async () => {
|
|
const client = {
|
|
checkoutForgeSetAutoMerge: vi.fn(async () => ({
|
|
enabled: true,
|
|
success: true,
|
|
error: null,
|
|
})),
|
|
};
|
|
useSessionStore.getState().initializeSession(serverId, client as unknown as DaemonClient);
|
|
useSessionStore.getState().updateSessionServerInfo(serverId, {
|
|
serverId,
|
|
hostname: null,
|
|
version: null,
|
|
features: {},
|
|
});
|
|
|
|
await expect(
|
|
useCheckoutGitActionsStore.getState().enablePrAutoMerge({ serverId, cwd, method: "merge" }),
|
|
).rejects.toThrow("Update the host to use auto-merge actions.");
|
|
|
|
expect(client.checkoutForgeSetAutoMerge).not.toHaveBeenCalled();
|
|
expect(
|
|
useCheckoutGitActionsStore
|
|
.getState()
|
|
.getStatus({ serverId, cwd, actionId: "enable-pr-auto-merge-merge" }),
|
|
).toBe("idle");
|
|
});
|
|
});
|