* Extract client SDK package * Polish SDK client identity defaults * Build client before dependent CI jobs * Restore daemon client server export * Extract protocol and client SDK packages * Fix provider override schema validation * Fix app test daemon client imports * Simplify workspace build targets * Fix CLI test server build bootstrap * Run SDK package tests in CI * Fix rebase package split drift * Restore lockfile registry metadata * Update SDK config test for prompt default * Move terminal stream router test to client package * Fix rebase drift for protocol imports * Fix SDK agent capability fixture * Restore legacy server client exports * Fix server export compatibility test * Advertise custom mode icon client capability * Remove server daemon-client exports * Format rebased mode control import * Fix rebase drift for protocol imports Files added by upstream PRs (#893, #1147, #1154) referenced the pre-split shared/ paths that this branch moves into @getpaseo/protocol. Redirect those imports to the protocol package so typecheck stays green after the rebase.
860 lines
26 KiB
TypeScript
860 lines
26 KiB
TypeScript
/**
|
|
* @vitest-environment jsdom
|
|
*/
|
|
import React, { type ReactNode } from "react";
|
|
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type {
|
|
DaemonClient,
|
|
FetchRecentProviderSessionEntry,
|
|
} from "@getpaseo/client/internal/daemon-client";
|
|
import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { ImportSessionSheet } from "@/components/import-session-sheet";
|
|
|
|
const { theme } = vi.hoisted(() => ({
|
|
theme: {
|
|
spacing: { 1: 4, 1.5: 6, 2: 8, 3: 12, 4: 16, 6: 24 },
|
|
borderWidth: { 1: 1 },
|
|
borderRadius: { md: 6, lg: 8, full: 9999 },
|
|
fontSize: { xs: 11, sm: 13, base: 15 },
|
|
fontWeight: { normal: "400", medium: "500", semibold: "600" },
|
|
iconSize: { sm: 14, md: 16 },
|
|
opacity: { 50: 0.5 },
|
|
colors: {
|
|
foreground: "#fff",
|
|
foregroundMuted: "#aaa",
|
|
surface0: "#000",
|
|
surface1: "#111",
|
|
surface2: "#222",
|
|
surface3: "#333",
|
|
border: "#444",
|
|
borderAccent: "#555",
|
|
},
|
|
},
|
|
}));
|
|
|
|
vi.hoisted(() => {
|
|
(globalThis as unknown as { __DEV__: boolean }).__DEV__ = false;
|
|
});
|
|
|
|
vi.mock("react-native-unistyles", () => ({
|
|
StyleSheet: {
|
|
create: (factory: unknown) => (typeof factory === "function" ? factory(theme) : factory),
|
|
},
|
|
useUnistyles: () => ({ theme }),
|
|
withUnistyles:
|
|
(Component: React.ComponentType<Record<string, unknown>>) =>
|
|
({
|
|
uniProps,
|
|
...rest
|
|
}: {
|
|
uniProps?: (theme: unknown) => Record<string, unknown>;
|
|
} & Record<string, unknown>) => {
|
|
const themed = uniProps ? uniProps(theme) : {};
|
|
return React.createElement(Component, { ...rest, ...themed });
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/constants/layout", () => ({
|
|
useIsCompactFormFactor: () => false,
|
|
}));
|
|
|
|
vi.mock("@/components/provider-icons", () => ({
|
|
getProviderIcon: () => () => null,
|
|
}));
|
|
|
|
vi.mock("lucide-react-native", () => {
|
|
const icon = (name: string) => {
|
|
const Icon = () => React.createElement("span", { "data-icon": name });
|
|
Icon.displayName = name;
|
|
return Icon;
|
|
};
|
|
return {
|
|
Inbox: icon("Inbox"),
|
|
RotateCw: icon("RotateCw"),
|
|
};
|
|
});
|
|
|
|
vi.mock("@/components/ui/loading-spinner", () => ({
|
|
LoadingSpinner: () =>
|
|
React.createElement("span", { "data-testid": "import-session-loading-spinner" }),
|
|
}));
|
|
|
|
vi.mock("@/components/ui/segmented-control", () => ({
|
|
SegmentedControl: ({
|
|
options,
|
|
value,
|
|
onValueChange,
|
|
testID,
|
|
}: {
|
|
options: ReadonlyArray<{ value: string; label: string; testID?: string }>;
|
|
value: string;
|
|
onValueChange: (value: string) => void;
|
|
testID?: string;
|
|
}) =>
|
|
React.createElement(
|
|
"div",
|
|
{ "data-testid": testID },
|
|
options.map((option) =>
|
|
React.createElement(
|
|
"button",
|
|
{
|
|
key: option.value,
|
|
type: "button",
|
|
"data-testid": option.testID,
|
|
"data-selected": value === option.value,
|
|
onClick: () => onValueChange(option.value),
|
|
},
|
|
option.label,
|
|
),
|
|
),
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/adaptive-modal-sheet", () => ({
|
|
AdaptiveModalSheet: ({
|
|
visible,
|
|
header,
|
|
children,
|
|
testID,
|
|
}: {
|
|
visible: boolean;
|
|
header?: { title: string; actions?: ReactNode };
|
|
children: ReactNode;
|
|
testID?: string;
|
|
}) =>
|
|
visible ? (
|
|
<section data-testid={testID}>
|
|
<h1>{header?.title}</h1>
|
|
{header?.actions}
|
|
{children}
|
|
</section>
|
|
) : null,
|
|
}));
|
|
|
|
vi.mock("react-native", async () => {
|
|
const actual = await vi.importActual<Record<string, unknown>>("react-native");
|
|
return actual;
|
|
});
|
|
|
|
const mockSnapshot = vi.hoisted(() => ({
|
|
current: {
|
|
entries: undefined as ProviderSnapshotEntry[] | undefined,
|
|
supportsSnapshot: false,
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/hooks/use-providers-snapshot", () => ({
|
|
useProvidersSnapshot: () => ({
|
|
entries: mockSnapshot.current.entries,
|
|
isLoading: false,
|
|
isFetching: false,
|
|
isRefreshing: false,
|
|
error: null,
|
|
supportsSnapshot: mockSnapshot.current.supportsSnapshot,
|
|
refresh: vi.fn(),
|
|
refetchIfStale: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
interface RenderOptions {
|
|
visible?: boolean;
|
|
onClose?: () => void;
|
|
onImportedAgent?: (agentId: string) => void;
|
|
onImported?: (agent: Awaited<ReturnType<DaemonClient["importAgent"]>>) => void;
|
|
cwd?: string | null;
|
|
snapshot?: {
|
|
entries?: ProviderSnapshotEntry[];
|
|
supportsSnapshot?: boolean;
|
|
};
|
|
}
|
|
|
|
function renderSheet(
|
|
client: Pick<DaemonClient, "fetchRecentProviderSessions" | "importAgent">,
|
|
options?: RenderOptions,
|
|
) {
|
|
mockSnapshot.current = {
|
|
entries: options?.snapshot?.entries,
|
|
supportsSnapshot: options?.snapshot?.supportsSnapshot ?? false,
|
|
};
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
|
|
const cwd = options && "cwd" in options ? (options.cwd ?? undefined) : "/repo/paseo";
|
|
|
|
return render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<ImportSessionSheet
|
|
visible={options?.visible ?? true}
|
|
client={client}
|
|
serverId="server-1"
|
|
cwd={cwd}
|
|
onClose={options?.onClose ?? vi.fn()}
|
|
onImportedAgent={options?.onImportedAgent ?? vi.fn()}
|
|
onImported={options?.onImported}
|
|
/>
|
|
</QueryClientProvider>,
|
|
);
|
|
}
|
|
|
|
function createRecentSessionsClient(
|
|
fetchRecentProviderSessions: Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions"
|
|
>["fetchRecentProviderSessions"],
|
|
importAgent: Pick<DaemonClient, "importAgent">["importAgent"],
|
|
): Pick<DaemonClient, "fetchRecentProviderSessions" | "importAgent"> {
|
|
return { fetchRecentProviderSessions, importAgent };
|
|
}
|
|
|
|
function createImportedAgentSnapshot(id: string): Awaited<ReturnType<DaemonClient["importAgent"]>> {
|
|
return {
|
|
id,
|
|
provider: "custom-provider",
|
|
cwd: "/repo/paseo",
|
|
model: null,
|
|
createdAt: "2026-04-30T10:00:00.000Z",
|
|
updatedAt: "2026-04-30T10:00:00.000Z",
|
|
lastUserMessageAt: "2026-04-30T10:00:00.000Z",
|
|
status: "idle",
|
|
capabilities: {
|
|
supportsStreaming: true,
|
|
supportsSessionPersistence: true,
|
|
supportsDynamicModes: false,
|
|
supportsMcpServers: false,
|
|
supportsReasoningStream: false,
|
|
supportsToolInvocations: true,
|
|
},
|
|
currentModeId: null,
|
|
availableModes: [],
|
|
pendingPermissions: [],
|
|
persistence: null,
|
|
title: null,
|
|
labels: {},
|
|
};
|
|
}
|
|
|
|
function createProviderSessionEntry(
|
|
overrides?: Partial<FetchRecentProviderSessionEntry>,
|
|
): FetchRecentProviderSessionEntry {
|
|
return {
|
|
providerId: "custom-provider",
|
|
providerLabel: "Custom Agent",
|
|
providerHandleId: "provider-thread-1",
|
|
cwd: "/repo/paseo",
|
|
title: "Import me",
|
|
firstPromptPreview: "Import this external provider session",
|
|
lastPromptPreview: "Import this external provider session",
|
|
lastActivityAt: "2026-04-30T10:00:00.000Z",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
const PROVIDER_LABELS: Record<string, string> = {
|
|
claude: "Claude Code",
|
|
codex: "Codex",
|
|
opencode: "OpenCode",
|
|
};
|
|
|
|
function createSnapshotEntry(
|
|
provider: string,
|
|
overrides?: Partial<ProviderSnapshotEntry>,
|
|
): ProviderSnapshotEntry {
|
|
return {
|
|
provider,
|
|
status: "ready",
|
|
enabled: true,
|
|
label: PROVIDER_LABELS[provider] ?? provider,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("ImportSessionSheet", () => {
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("shows an update-host message when the daemon does not support provider snapshots", async () => {
|
|
const fetchRecentProviderSessions = vi.fn();
|
|
const importAgent = vi.fn();
|
|
|
|
renderSheet({ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>);
|
|
|
|
await screen.findByText("Update the host to import sessions.");
|
|
expect(fetchRecentProviderSessions).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows a loading state while provider snapshot is loading", async () => {
|
|
const fetchRecentProviderSessions = vi.fn(
|
|
() => new Promise<Awaited<ReturnType<DaemonClient["fetchRecentProviderSessions"]>>>(() => {}),
|
|
);
|
|
const importAgent = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
snapshot: { supportsSnapshot: true, entries: undefined },
|
|
},
|
|
);
|
|
|
|
await screen.findByText("Loading recent sessions...");
|
|
expect(fetchRecentProviderSessions).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows an empty state when there are no recent provider sessions to import", async () => {
|
|
const fetchRecentProviderSessions = vi.fn(async () => ({
|
|
requestId: "recent-provider-sessions",
|
|
entries: [],
|
|
}));
|
|
const importAgent = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
snapshot: { supportsSnapshot: true, entries: [createSnapshotEntry("claude")] },
|
|
},
|
|
);
|
|
|
|
await screen.findByText("No recent sessions to import.");
|
|
});
|
|
|
|
it("shows the all-already-imported empty state when filteredAlreadyImportedCount is positive", async () => {
|
|
const fetchRecentProviderSessions = vi.fn(async () => ({
|
|
requestId: "recent-provider-sessions",
|
|
entries: [],
|
|
filteredAlreadyImportedCount: 3,
|
|
}));
|
|
const importAgent = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
snapshot: { supportsSnapshot: true, entries: [createSnapshotEntry("claude")] },
|
|
},
|
|
);
|
|
|
|
await screen.findByText("All recent sessions are already imported.");
|
|
expect(screen.queryByText("No recent sessions to import.")).toBeNull();
|
|
});
|
|
|
|
it("shows a fetch error state when recent provider sessions cannot be loaded", async () => {
|
|
const fetchRecentProviderSessions = vi.fn(async () => {
|
|
throw new Error("recent sessions unavailable");
|
|
});
|
|
const importAgent = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
snapshot: { supportsSnapshot: true, entries: [createSnapshotEntry("claude")] },
|
|
},
|
|
);
|
|
|
|
await screen.findByText("Could not load recent sessions.");
|
|
});
|
|
|
|
it("loads recent provider sessions for the workspace and renders descriptor-owned labels", async () => {
|
|
vi.setSystemTime(new Date("2026-04-30T12:00:00.000Z"));
|
|
const fetchRecentProviderSessions = vi.fn(async () => ({
|
|
requestId: "recent-provider-sessions",
|
|
entries: [
|
|
createProviderSessionEntry({
|
|
providerId: "claude",
|
|
providerLabel: "Claude Code",
|
|
title: null,
|
|
firstPromptPreview: "Implement the importer sheet",
|
|
lastPromptPreview: "Make the rows readable and provider opaque",
|
|
}),
|
|
],
|
|
}));
|
|
const importAgent = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
snapshot: { supportsSnapshot: true, entries: [createSnapshotEntry("claude")] },
|
|
},
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(fetchRecentProviderSessions).toHaveBeenCalledWith({
|
|
cwd: "/repo/paseo",
|
|
providers: ["claude"],
|
|
limit: 15,
|
|
});
|
|
});
|
|
|
|
await screen.findByText("Implement the importer sheet");
|
|
screen.getByText("2h ago");
|
|
screen.getByText("Make the rows readable and provider opaque");
|
|
});
|
|
|
|
it("keeps cached rows visible and revalidates when reopened", async () => {
|
|
const fetchRecentProviderSessions = vi.fn(async () => ({
|
|
requestId: "recent-provider-sessions",
|
|
entries: [
|
|
createProviderSessionEntry({
|
|
providerId: "claude",
|
|
providerLabel: "Claude Code",
|
|
title: "Cached importable session",
|
|
}),
|
|
],
|
|
}));
|
|
const importAgent = vi.fn();
|
|
const client = createRecentSessionsClient(fetchRecentProviderSessions, importAgent);
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
mockSnapshot.current = {
|
|
entries: [createSnapshotEntry("claude")],
|
|
supportsSnapshot: true,
|
|
};
|
|
|
|
function TestSheet({ visible }: { visible: boolean }) {
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<ImportSessionSheet
|
|
visible={visible}
|
|
client={client}
|
|
serverId="server-1"
|
|
cwd="/repo/paseo"
|
|
onClose={vi.fn()}
|
|
onImportedAgent={vi.fn()}
|
|
/>
|
|
</QueryClientProvider>
|
|
);
|
|
}
|
|
|
|
const { rerender } = render(<TestSheet visible />);
|
|
|
|
await screen.findByText("Cached importable session");
|
|
expect(fetchRecentProviderSessions).toHaveBeenCalledTimes(1);
|
|
|
|
rerender(<TestSheet visible={false} />);
|
|
fetchRecentProviderSessions.mockClear();
|
|
rerender(<TestSheet visible />);
|
|
|
|
await screen.findByText("Cached importable session");
|
|
await waitFor(() => {
|
|
expect(fetchRecentProviderSessions).toHaveBeenCalledWith({
|
|
cwd: "/repo/paseo",
|
|
providers: ["claude"],
|
|
limit: 15,
|
|
});
|
|
});
|
|
});
|
|
|
|
it("imports a selected session by provider handle and reports the imported agent", async () => {
|
|
const fetchRecentProviderSessions = vi.fn(async () => ({
|
|
requestId: "recent-provider-sessions",
|
|
entries: [createProviderSessionEntry({ providerId: "claude", providerLabel: "Claude Code" })],
|
|
}));
|
|
const importAgent = vi.fn(async () => createImportedAgentSnapshot("agent-imported"));
|
|
const onClose = vi.fn();
|
|
const onImportedAgent = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
onClose,
|
|
onImportedAgent,
|
|
snapshot: { supportsSnapshot: true, entries: [createSnapshotEntry("claude")] },
|
|
},
|
|
);
|
|
|
|
fireEvent.click(await screen.findByTestId("import-session-session-claude-provider-thread-1"));
|
|
|
|
await waitFor(() => {
|
|
expect(importAgent).toHaveBeenCalledWith({
|
|
providerId: "claude",
|
|
providerHandleId: "provider-thread-1",
|
|
cwd: "/repo/paseo",
|
|
});
|
|
});
|
|
expect(onImportedAgent).toHaveBeenCalledWith("agent-imported");
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("shows an import error state without closing when selected session import fails", async () => {
|
|
const fetchRecentProviderSessions = vi.fn(async () => ({
|
|
requestId: "recent-provider-sessions",
|
|
entries: [createProviderSessionEntry({ providerId: "claude", providerLabel: "Claude Code" })],
|
|
}));
|
|
const importAgent = vi.fn(async () => {
|
|
throw new Error("import unavailable");
|
|
});
|
|
const onClose = vi.fn();
|
|
const onImportedAgent = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
onClose,
|
|
onImportedAgent,
|
|
snapshot: { supportsSnapshot: true, entries: [createSnapshotEntry("claude")] },
|
|
},
|
|
);
|
|
|
|
fireEvent.click(await screen.findByTestId("import-session-session-claude-provider-thread-1"));
|
|
|
|
await screen.findByText("Could not import selected session.");
|
|
expect(importAgent).toHaveBeenCalledWith({
|
|
providerId: "claude",
|
|
providerHandleId: "provider-thread-1",
|
|
cwd: "/repo/paseo",
|
|
});
|
|
expect(onImportedAgent).not.toHaveBeenCalled();
|
|
expect(onClose).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fans out one request per enabled importable provider when snapshot is supported", async () => {
|
|
const fetchRecentProviderSessions = vi.fn(
|
|
async (options: { providers?: string[] } | undefined) => ({
|
|
requestId: `recent-${options?.providers?.[0] ?? "all"}`,
|
|
entries: [
|
|
createProviderSessionEntry({
|
|
providerId: options?.providers?.[0] ?? "custom-provider",
|
|
providerLabel: options?.providers?.[0] ?? "Custom",
|
|
providerHandleId: `${options?.providers?.[0] ?? "custom-provider"}-thread`,
|
|
title: `Session ${options?.providers?.[0] ?? "all"}`,
|
|
lastActivityAt: "2026-04-30T10:00:00.000Z",
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
const importAgent = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
snapshot: {
|
|
supportsSnapshot: true,
|
|
entries: [
|
|
createSnapshotEntry("claude"),
|
|
createSnapshotEntry("codex"),
|
|
createSnapshotEntry("opencode", { enabled: false }),
|
|
createSnapshotEntry("z-ai"),
|
|
],
|
|
},
|
|
},
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(fetchRecentProviderSessions).toHaveBeenCalledWith({
|
|
cwd: "/repo/paseo",
|
|
providers: ["claude"],
|
|
limit: 15,
|
|
});
|
|
});
|
|
expect(fetchRecentProviderSessions).toHaveBeenCalledWith({
|
|
cwd: "/repo/paseo",
|
|
providers: ["codex"],
|
|
limit: 15,
|
|
});
|
|
expect(fetchRecentProviderSessions).not.toHaveBeenCalledWith(
|
|
expect.objectContaining({ providers: ["opencode"] }),
|
|
);
|
|
expect(fetchRecentProviderSessions).not.toHaveBeenCalledWith(
|
|
expect.objectContaining({ providers: ["z-ai"] }),
|
|
);
|
|
|
|
await screen.findByText("Session claude");
|
|
await screen.findByText("Session codex");
|
|
});
|
|
|
|
it("shows partial-failure note when one provider request fails but others succeed", async () => {
|
|
const fetchRecentProviderSessions = vi.fn(
|
|
async (options: { providers?: string[] } | undefined) => {
|
|
const provider = options?.providers?.[0];
|
|
if (provider === "claude") {
|
|
throw new Error("claude offline");
|
|
}
|
|
return {
|
|
requestId: `recent-${provider ?? "all"}`,
|
|
entries: [
|
|
createProviderSessionEntry({
|
|
providerId: provider ?? "custom-provider",
|
|
providerHandleId: `${provider}-thread`,
|
|
providerLabel: provider ?? "Custom",
|
|
title: `Session ${provider}`,
|
|
}),
|
|
],
|
|
};
|
|
},
|
|
);
|
|
const importAgent = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
snapshot: {
|
|
supportsSnapshot: true,
|
|
entries: [createSnapshotEntry("claude"), createSnapshotEntry("codex")],
|
|
},
|
|
},
|
|
);
|
|
|
|
await screen.findByText("Session codex");
|
|
await screen.findByText("Could not load sessions for Claude Code.");
|
|
});
|
|
|
|
it("filters the merged list when a provider badge is selected and restores it on All", async () => {
|
|
const fetchRecentProviderSessions = vi.fn(
|
|
async (options: { providers?: string[] } | undefined) => {
|
|
const provider = options?.providers?.[0] ?? "claude";
|
|
return {
|
|
requestId: `recent-${provider}`,
|
|
entries: [
|
|
createProviderSessionEntry({
|
|
providerId: provider,
|
|
providerLabel: provider === "claude" ? "Claude Code" : "Codex",
|
|
providerHandleId: `${provider}-thread`,
|
|
title: `Session ${provider}`,
|
|
lastActivityAt:
|
|
provider === "claude" ? "2026-04-30T09:00:00.000Z" : "2026-04-30T10:00:00.000Z",
|
|
}),
|
|
],
|
|
};
|
|
},
|
|
);
|
|
const importAgent = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
snapshot: {
|
|
supportsSnapshot: true,
|
|
entries: [createSnapshotEntry("claude"), createSnapshotEntry("codex")],
|
|
},
|
|
},
|
|
);
|
|
|
|
await screen.findByText("Session claude");
|
|
await screen.findByText("Session codex");
|
|
|
|
fireEvent.click(screen.getByTestId("import-session-filter-codex"));
|
|
|
|
screen.getByText("Session codex");
|
|
expect(screen.queryByText("Session claude")).toBeNull();
|
|
|
|
fireEvent.click(screen.getByTestId("import-session-filter-all"));
|
|
|
|
screen.getByText("Session claude");
|
|
screen.getByText("Session codex");
|
|
});
|
|
|
|
it("does not render filter badges when only one importable provider is enabled", async () => {
|
|
const fetchRecentProviderSessions = vi.fn(async () => ({
|
|
requestId: "recent-codex",
|
|
entries: [createProviderSessionEntry({ providerId: "codex", providerLabel: "Codex" })],
|
|
}));
|
|
const importAgent = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
snapshot: {
|
|
supportsSnapshot: true,
|
|
entries: [
|
|
createSnapshotEntry("codex"),
|
|
createSnapshotEntry("claude", { enabled: false }),
|
|
],
|
|
},
|
|
},
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(fetchRecentProviderSessions).toHaveBeenCalled();
|
|
});
|
|
expect(screen.queryByTestId("import-session-filters")).toBeNull();
|
|
expect(screen.queryByTestId("import-session-filter-all")).toBeNull();
|
|
});
|
|
|
|
it("shows a no-importable-providers message when snapshot has no enabled importable providers", async () => {
|
|
const fetchRecentProviderSessions = vi.fn();
|
|
const importAgent = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
snapshot: {
|
|
supportsSnapshot: true,
|
|
entries: [
|
|
createSnapshotEntry("claude", { enabled: false }),
|
|
createSnapshotEntry("codex", { enabled: false }),
|
|
createSnapshotEntry("opencode", { enabled: false }),
|
|
createSnapshotEntry("z-ai"),
|
|
],
|
|
},
|
|
},
|
|
);
|
|
|
|
await screen.findByText("No importable providers are enabled.");
|
|
expect(fetchRecentProviderSessions).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("omits cwd from fetch and renders the session cwd on each row when cwd is unset", async () => {
|
|
const fetchRecentProviderSessions = vi.fn(async () => ({
|
|
requestId: "recent-provider-sessions",
|
|
entries: [
|
|
createProviderSessionEntry({
|
|
providerId: "claude",
|
|
providerLabel: "Claude Code",
|
|
cwd: "/home/me/work/other-project",
|
|
title: "Cross-project session",
|
|
}),
|
|
],
|
|
}));
|
|
const importAgent = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
cwd: null,
|
|
snapshot: { supportsSnapshot: true, entries: [createSnapshotEntry("claude")] },
|
|
},
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(fetchRecentProviderSessions).toHaveBeenCalledWith({
|
|
providers: ["claude"],
|
|
limit: 15,
|
|
});
|
|
});
|
|
expect(fetchRecentProviderSessions).not.toHaveBeenCalledWith(
|
|
expect.objectContaining({ cwd: expect.anything() }),
|
|
);
|
|
await screen.findByText("/home/me/work/other-project");
|
|
});
|
|
|
|
it("uses the session's cwd when importing in cwd-less mode and fires onImported", async () => {
|
|
const fetchRecentProviderSessions = vi.fn(async () => ({
|
|
requestId: "recent-provider-sessions",
|
|
entries: [
|
|
createProviderSessionEntry({
|
|
providerId: "claude",
|
|
providerLabel: "Claude Code",
|
|
cwd: "/home/me/work/other-project",
|
|
}),
|
|
],
|
|
}));
|
|
const importAgent = vi.fn(async () => createImportedAgentSnapshot("agent-imported"));
|
|
const onImported = vi.fn();
|
|
const onImportedAgent = vi.fn();
|
|
const onClose = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
cwd: null,
|
|
onClose,
|
|
onImported,
|
|
onImportedAgent,
|
|
snapshot: { supportsSnapshot: true, entries: [createSnapshotEntry("claude")] },
|
|
},
|
|
);
|
|
|
|
fireEvent.click(await screen.findByTestId("import-session-session-claude-provider-thread-1"));
|
|
|
|
await waitFor(() => {
|
|
expect(importAgent).toHaveBeenCalledWith({
|
|
providerId: "claude",
|
|
providerHandleId: "provider-thread-1",
|
|
cwd: "/home/me/work/other-project",
|
|
});
|
|
});
|
|
expect(onImported).toHaveBeenCalledTimes(1);
|
|
expect(onImported).toHaveBeenCalledWith(expect.objectContaining({ id: "agent-imported" }));
|
|
expect(onImportedAgent).toHaveBeenCalledWith("agent-imported");
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("refetches sessions when the refresh button is clicked", async () => {
|
|
const fetchRecentProviderSessions = vi.fn(async () => ({
|
|
requestId: "recent-provider-sessions",
|
|
entries: [
|
|
createProviderSessionEntry({
|
|
providerId: "claude",
|
|
providerLabel: "Claude Code",
|
|
title: "Refreshable session",
|
|
}),
|
|
],
|
|
}));
|
|
const importAgent = vi.fn();
|
|
|
|
renderSheet(
|
|
{ fetchRecentProviderSessions, importAgent } as Pick<
|
|
DaemonClient,
|
|
"fetchRecentProviderSessions" | "importAgent"
|
|
>,
|
|
{
|
|
snapshot: { supportsSnapshot: true, entries: [createSnapshotEntry("claude")] },
|
|
},
|
|
);
|
|
|
|
await screen.findByText("Refreshable session");
|
|
expect(fetchRecentProviderSessions).toHaveBeenCalledTimes(1);
|
|
|
|
fireEvent.click(screen.getByTestId("import-session-refresh"));
|
|
|
|
await waitFor(() => {
|
|
expect(fetchRecentProviderSessions).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
});
|