mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
Add an "Allow local sandboxes" checkbox (checked by default) below the Docker/Daytona choice in the web installer, and stop unconditionally enabling all three providers when generating settings.toml. The wizard now enables only the selected runtime plus local when allowed; the unselected runtime is written as `enabled = false` so the config resolver does not default it back on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1288 lines
44 KiB
TypeScript
1288 lines
44 KiB
TypeScript
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
import type { AxiosAdapter } from "axios";
|
|
import { StrictMode } from "react";
|
|
import { MemoryRouter, Route, Routes } from "react-router";
|
|
import TestRenderer, { act } from "react-test-renderer";
|
|
|
|
import InstallApp from "./install-app";
|
|
import { generatedAxios } from "./lib/api-client";
|
|
|
|
const INSTALL_ERROR_MESSAGE =
|
|
"GitHub App setup failed before Fabro could save the app credentials. Continue again to retry the callback.";
|
|
const INSTALL_PREFILL = {
|
|
canonical_url: "https://fabro.example.com",
|
|
object_store_local_root: "/home/test/.fabro/storage/objects",
|
|
};
|
|
|
|
const SESSION_RESPONSE = {
|
|
completed_steps: ["server", "object_store", "llm"],
|
|
llm: null,
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: { provider: "local" },
|
|
github: null,
|
|
prefill: INSTALL_PREFILL,
|
|
};
|
|
|
|
const originalAdapter = generatedAxios.defaults.adapter;
|
|
|
|
type TestWindow = {
|
|
history: {
|
|
state: unknown;
|
|
replaceState: (state: unknown, unused: string, url?: string | URL | null) => void;
|
|
};
|
|
location: {
|
|
href: string;
|
|
pathname: string;
|
|
search: string;
|
|
};
|
|
sessionStorage: {
|
|
clear: () => void;
|
|
getItem: (key: string) => string | null;
|
|
removeItem: (key: string) => void;
|
|
setItem: (key: string, value: string) => void;
|
|
};
|
|
setInterval: typeof setInterval;
|
|
clearInterval: typeof clearInterval;
|
|
setTimeout: typeof setTimeout;
|
|
clearTimeout: typeof clearTimeout;
|
|
};
|
|
|
|
function createTestWindow(initialUrl: string): TestWindow {
|
|
let current = new URL(initialUrl);
|
|
const sessionStorage = new Map<string, string>();
|
|
|
|
const location = {
|
|
get href() {
|
|
return current.toString();
|
|
},
|
|
set href(value: string) {
|
|
current = new URL(value, current.origin);
|
|
},
|
|
get pathname() {
|
|
return current.pathname;
|
|
},
|
|
get search() {
|
|
return current.search;
|
|
},
|
|
};
|
|
|
|
return {
|
|
history: {
|
|
state: null,
|
|
replaceState(state, _unused, url) {
|
|
this.state = state;
|
|
if (url) {
|
|
current = new URL(String(url), current.origin);
|
|
}
|
|
},
|
|
},
|
|
location,
|
|
sessionStorage: {
|
|
clear() {
|
|
sessionStorage.clear();
|
|
},
|
|
getItem(key) {
|
|
return sessionStorage.get(key) ?? null;
|
|
},
|
|
removeItem(key) {
|
|
sessionStorage.delete(key);
|
|
},
|
|
setItem(key, value) {
|
|
sessionStorage.set(key, value);
|
|
},
|
|
},
|
|
setInterval,
|
|
clearInterval,
|
|
setTimeout,
|
|
clearTimeout,
|
|
};
|
|
}
|
|
|
|
function renderTreeText(
|
|
node: ReturnType<TestRenderer.ReactTestRenderer["toJSON"]>,
|
|
): string {
|
|
if (!node) return "";
|
|
if (typeof node === "string") return node;
|
|
if (Array.isArray(node)) return node.map(renderTreeText).join("");
|
|
return (node.children ?? []).map(renderTreeText).join("");
|
|
}
|
|
|
|
function findOptionButton(
|
|
renderer: TestRenderer.ReactTestRenderer,
|
|
title: string,
|
|
): TestRenderer.ReactTestInstance {
|
|
const button = renderer.root.findAll((node) => {
|
|
if (node.type !== "button" || node.props["aria-pressed"] === undefined) {
|
|
return false;
|
|
}
|
|
const titleSpan = node.findAll(
|
|
(child) => child.type === "span" && child.children[0] === title,
|
|
);
|
|
return titleSpan.length > 0;
|
|
})[0];
|
|
if (!button) {
|
|
throw new Error(`option button "${title}" not found`);
|
|
}
|
|
return button;
|
|
}
|
|
|
|
function useInstallFetchMock(fetchMock: typeof fetch) {
|
|
generatedAxios.defaults.adapter = (async (config) => {
|
|
const response = await fetchMock(config.url ?? "", {
|
|
method: config.method?.toUpperCase(),
|
|
headers: config.headers as HeadersInit,
|
|
body: config.data as BodyInit | undefined,
|
|
signal: config.signal,
|
|
});
|
|
const text = response.status === 204 ? "" : await response.text();
|
|
const data = text ? JSON.parse(text) : undefined;
|
|
const axiosResponse = {
|
|
data,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: Object.fromEntries(response.headers.entries()),
|
|
config,
|
|
};
|
|
if (response.status >= 400) {
|
|
throw {
|
|
isAxiosError: true,
|
|
message: response.statusText || `HTTP ${response.status}`,
|
|
response: axiosResponse,
|
|
};
|
|
}
|
|
return axiosResponse;
|
|
}) as AxiosAdapter;
|
|
}
|
|
|
|
async function waitFor(assertion: () => void, timeoutMs = 1000): Promise<void> {
|
|
const deadline = Date.now() + timeoutMs;
|
|
let lastError: unknown;
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
assertion();
|
|
return;
|
|
} catch (error) {
|
|
lastError = error;
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
}
|
|
throw lastError;
|
|
}
|
|
|
|
describe("InstallApp", () => {
|
|
afterEach(() => {
|
|
generatedAxios.defaults.adapter = originalAdapter;
|
|
delete (globalThis as { window?: unknown }).window;
|
|
delete (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT;
|
|
mock.restore();
|
|
});
|
|
|
|
test("renders the GitHub callback error on the GitHub install step", async () => {
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
try {
|
|
const fetchMock = mock((input: RequestInfo | URL) => {
|
|
expect(String(input)).toBe("/install/session");
|
|
return Promise.resolve(
|
|
new Response(JSON.stringify(SESSION_RESPONSE), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
);
|
|
});
|
|
useInstallFetchMock(fetchMock as typeof fetch);
|
|
|
|
const testWindow = createTestWindow(
|
|
"https://fabro.example.com/install/github?error=github-app-manifest-conversion-failed",
|
|
);
|
|
testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
|
|
(globalThis as { window?: unknown }).window = testWindow;
|
|
|
|
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
await act(async () => {
|
|
renderer = TestRenderer.create(
|
|
<StrictMode>
|
|
<MemoryRouter initialEntries={["/install/github?error=github-app-manifest-conversion-failed"]}>
|
|
<Routes>
|
|
<Route path="/install/*" element={<InstallApp />} />
|
|
</Routes>
|
|
</MemoryRouter>
|
|
</StrictMode>,
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(renderTreeText(renderer!.toJSON())).toContain(INSTALL_ERROR_MESSAGE);
|
|
});
|
|
expect(testWindow.location.search).toBe("");
|
|
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
});
|
|
|
|
test("renders the GitHub App done screen after the manifest callback resolves", async () => {
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
try {
|
|
// Simulate the server-side /install/github/app/redirect handler having
|
|
// just run — session returns a fully-populated `github.app` payload.
|
|
const sessionResponse = {
|
|
completed_steps: ["server", "object_store", "llm", "github"],
|
|
llm: null,
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: { provider: "local" },
|
|
github: {
|
|
strategy: "app",
|
|
owner: { kind: "personal" },
|
|
app_name: "Fabro",
|
|
slug: "fabro-brynary",
|
|
allowed_username: "brynary",
|
|
},
|
|
prefill: INSTALL_PREFILL,
|
|
};
|
|
const fetchMock = mock((input: RequestInfo | URL) => {
|
|
expect(String(input)).toBe("/install/session");
|
|
return Promise.resolve(
|
|
new Response(JSON.stringify(sessionResponse), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
);
|
|
});
|
|
useInstallFetchMock(fetchMock as typeof fetch);
|
|
|
|
const testWindow = createTestWindow(
|
|
"https://fabro.example.com/install/github/done?token=test-install-token",
|
|
);
|
|
testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
|
|
(globalThis as { window?: unknown }).window = testWindow;
|
|
|
|
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
await act(async () => {
|
|
renderer = TestRenderer.create(
|
|
<MemoryRouter initialEntries={["/install/github/done?token=test-install-token"]}>
|
|
<Routes>
|
|
<Route path="/install/*" element={<InstallApp />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
const text = renderTreeText(renderer!.toJSON());
|
|
expect(text).toContain("GitHub App connected");
|
|
expect(text).toContain("fabro-brynary");
|
|
});
|
|
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
});
|
|
|
|
test("saves local disk object-store settings and advances to the sandbox step", async () => {
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
try {
|
|
const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
|
|
const fetchMock = mock((input: RequestInfo | URL, init?: RequestInit) => {
|
|
fetchCalls.push({ input, init });
|
|
if (String(input) === "/install/session" && fetchCalls.length === 1) {
|
|
return Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server"],
|
|
llm: null,
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: null,
|
|
github: null,
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
},
|
|
),
|
|
);
|
|
}
|
|
if (String(input) === "/install/object-store") {
|
|
return Promise.resolve(new Response(null, { status: 204 }));
|
|
}
|
|
if (String(input) === "/install/session" && fetchCalls.length === 3) {
|
|
return Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server", "object_store"],
|
|
llm: null,
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: { provider: "local" },
|
|
github: null,
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
},
|
|
),
|
|
);
|
|
}
|
|
throw new Error(`unexpected fetch: ${String(input)}`);
|
|
});
|
|
useInstallFetchMock(fetchMock as typeof fetch);
|
|
|
|
const testWindow = createTestWindow("https://fabro.example.com/install/object-store");
|
|
testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
|
|
(globalThis as { window?: unknown }).window = testWindow;
|
|
|
|
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
await act(async () => {
|
|
renderer = TestRenderer.create(
|
|
<MemoryRouter initialEntries={["/install/object-store"]}>
|
|
<Routes>
|
|
<Route path="/install/*" element={<InstallApp />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(renderTreeText(renderer!.toJSON())).toContain(
|
|
"Choose the shared object store",
|
|
);
|
|
});
|
|
|
|
const form = renderer!.root.findByType("form");
|
|
await act(async () => {
|
|
form.props.onSubmit({ preventDefault() {} });
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(renderTreeText(renderer!.toJSON())).toContain("Choose the sandbox runtime");
|
|
});
|
|
const backLink = renderer!.root.findAll(
|
|
(node) =>
|
|
node.type === "a" &&
|
|
node.props.href === "/install/object-store" &&
|
|
node.children.includes("Back"),
|
|
);
|
|
expect(backLink).toHaveLength(1);
|
|
expect(fetchCalls.map((call) => String(call.input))).toEqual([
|
|
"/install/session",
|
|
"/install/object-store",
|
|
"/install/session",
|
|
]);
|
|
expect(fetchCalls[1]?.init?.body).toBe(
|
|
JSON.stringify({
|
|
provider: "local",
|
|
root: INSTALL_PREFILL.object_store_local_root,
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
});
|
|
|
|
test("rehydrates saved manual S3 credentials without exposing the secrets", async () => {
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
try {
|
|
const fetchMock = mock((input: RequestInfo | URL) => {
|
|
expect(String(input)).toBe("/install/session");
|
|
return Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server", "object_store"],
|
|
llm: null,
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: {
|
|
provider: "s3",
|
|
bucket: "fabro-data",
|
|
region: "us-east-1",
|
|
credential_mode: "access_key",
|
|
manual_credentials_saved: true,
|
|
},
|
|
github: null,
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
},
|
|
),
|
|
);
|
|
});
|
|
useInstallFetchMock(fetchMock as typeof fetch);
|
|
|
|
const testWindow = createTestWindow("https://fabro.example.com/install/object-store");
|
|
testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
|
|
(globalThis as { window?: unknown }).window = testWindow;
|
|
|
|
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
await act(async () => {
|
|
renderer = TestRenderer.create(
|
|
<MemoryRouter initialEntries={["/install/object-store"]}>
|
|
<Routes>
|
|
<Route path="/install/*" element={<InstallApp />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(renderTreeText(renderer!.toJSON())).toContain(
|
|
"Credentials saved. Leave both fields blank to keep them, or enter both fields to replace them.",
|
|
);
|
|
});
|
|
|
|
expect(renderer!.root.findByProps({ name: "aws_access_key_id" }).props.value).toBe("");
|
|
expect(renderer!.root.findByProps({ name: "aws_secret_access_key" }).props.value).toBe("");
|
|
expect(renderTreeText(renderer!.toJSON())).not.toContain("AKIA");
|
|
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
});
|
|
|
|
test("shows the redacted object-store summary on the review step", async () => {
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
try {
|
|
const fetchMock = mock((input: RequestInfo | URL) => {
|
|
expect(String(input)).toBe("/install/session");
|
|
return Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server", "object_store", "llm", "github"],
|
|
llm: {
|
|
providers: [{ provider: "anthropic" }],
|
|
},
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: {
|
|
provider: "s3",
|
|
bucket: "fabro-data",
|
|
region: "us-east-1",
|
|
credential_mode: "access_key",
|
|
manual_credentials_saved: true,
|
|
},
|
|
github: { strategy: "token", username: "octocat" },
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
},
|
|
),
|
|
);
|
|
});
|
|
useInstallFetchMock(fetchMock as typeof fetch);
|
|
|
|
const testWindow = createTestWindow("https://fabro.example.com/install/review");
|
|
testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
|
|
(globalThis as { window?: unknown }).window = testWindow;
|
|
|
|
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
await act(async () => {
|
|
renderer = TestRenderer.create(
|
|
<MemoryRouter initialEntries={["/install/review"]}>
|
|
<Routes>
|
|
<Route path="/install/*" element={<InstallApp />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
const text = renderTreeText(renderer!.toJSON());
|
|
expect(text).toContain("AWS S3");
|
|
expect(text).toContain("fabro-data");
|
|
expect(text).toContain("us-east-1");
|
|
expect(text).toContain("Access key");
|
|
expect(text).toContain("slatedb/, artifacts/");
|
|
});
|
|
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
});
|
|
|
|
test("shows the sandbox provider on the review step", async () => {
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
try {
|
|
const fetchMock = mock(() =>
|
|
Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server", "object_store", "sandbox", "llm", "github"],
|
|
llm: { providers: [{ provider: "anthropic" }] },
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: { provider: "local" },
|
|
sandbox: { provider: "daytona", api_key_saved: true },
|
|
github: { strategy: "token", username: "octocat" },
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
},
|
|
),
|
|
),
|
|
);
|
|
useInstallFetchMock(fetchMock as typeof fetch);
|
|
|
|
const testWindow = createTestWindow("https://fabro.example.com/install/review");
|
|
testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
|
|
(globalThis as { window?: unknown }).window = testWindow;
|
|
|
|
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
await act(async () => {
|
|
renderer = TestRenderer.create(
|
|
<MemoryRouter initialEntries={["/install/review"]}>
|
|
<Routes>
|
|
<Route path="/install/*" element={<InstallApp />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
const text = renderTreeText(renderer!.toJSON());
|
|
expect(text).toContain("Daytona");
|
|
expect(text).toContain("Saved");
|
|
});
|
|
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
});
|
|
|
|
test("validates Daytona key, saves sandbox, and advances to the LLM step", async () => {
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
try {
|
|
const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
|
|
const fetchMock = mock((input: RequestInfo | URL, init?: RequestInit) => {
|
|
fetchCalls.push({ input, init });
|
|
if (String(input) === "/install/session" && fetchCalls.length === 1) {
|
|
return Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server", "object_store"],
|
|
llm: null,
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: { provider: "local" },
|
|
sandbox: null,
|
|
github: null,
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
},
|
|
),
|
|
);
|
|
}
|
|
if (
|
|
String(input) === "/install/sandbox/test"
|
|
|| String(input) === "/install/sandbox"
|
|
) {
|
|
return Promise.resolve(
|
|
new Response(JSON.stringify({ ok: true }), {
|
|
status: String(input).endsWith("/test") ? 200 : 204,
|
|
}),
|
|
);
|
|
}
|
|
if (String(input) === "/install/session") {
|
|
return Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server", "object_store", "sandbox"],
|
|
llm: null,
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: { provider: "local" },
|
|
sandbox: { provider: "daytona", api_key_saved: true },
|
|
github: null,
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
},
|
|
),
|
|
);
|
|
}
|
|
throw new Error(`unexpected fetch: ${String(input)}`);
|
|
});
|
|
useInstallFetchMock(fetchMock as typeof fetch);
|
|
|
|
const testWindow = createTestWindow("https://fabro.example.com/install/sandbox");
|
|
testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
|
|
(globalThis as { window?: unknown }).window = testWindow;
|
|
|
|
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
await act(async () => {
|
|
renderer = TestRenderer.create(
|
|
<MemoryRouter initialEntries={["/install/sandbox"]}>
|
|
<Routes>
|
|
<Route path="/install/*" element={<InstallApp />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(renderTreeText(renderer!.toJSON())).toContain("Choose the sandbox runtime");
|
|
});
|
|
|
|
const daytonaButton = findOptionButton(renderer!, "Daytona");
|
|
await act(async () => {
|
|
daytonaButton.props.onClick();
|
|
});
|
|
|
|
const apiKeyInput = renderer!.root.findByProps({ name: "sandbox_api_key" });
|
|
await act(async () => {
|
|
apiKeyInput.props.onChange({ target: { value: "dtn_secret" } });
|
|
});
|
|
|
|
const form = renderer!.root.findByType("form");
|
|
await act(async () => {
|
|
form.props.onSubmit({ preventDefault() {} });
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(renderTreeText(renderer!.toJSON())).toContain("Add your LLM credentials");
|
|
});
|
|
const calls = fetchCalls.map((call) => String(call.input));
|
|
const testIdx = calls.indexOf("/install/sandbox/test");
|
|
const putIdx = calls.indexOf("/install/sandbox");
|
|
expect(testIdx).toBeGreaterThanOrEqual(0);
|
|
expect(putIdx).toBeGreaterThan(testIdx);
|
|
const sandboxTestCall = fetchCalls[testIdx];
|
|
expect(sandboxTestCall?.init?.body).toBe(
|
|
JSON.stringify({ provider: "daytona", allow_local: true, api_key: "dtn_secret" }),
|
|
);
|
|
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
});
|
|
|
|
test("blocks Daytona save when the API key is missing", async () => {
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
try {
|
|
const fetchCalls: Array<{ input: RequestInfo | URL }> = [];
|
|
const fetchMock = mock((input: RequestInfo | URL) => {
|
|
fetchCalls.push({ input });
|
|
if (String(input) === "/install/session") {
|
|
return Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server", "object_store"],
|
|
llm: null,
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: { provider: "local" },
|
|
sandbox: null,
|
|
github: null,
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
},
|
|
),
|
|
);
|
|
}
|
|
throw new Error(`unexpected fetch: ${String(input)}`);
|
|
});
|
|
useInstallFetchMock(fetchMock as typeof fetch);
|
|
|
|
const testWindow = createTestWindow("https://fabro.example.com/install/sandbox");
|
|
testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
|
|
(globalThis as { window?: unknown }).window = testWindow;
|
|
|
|
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
await act(async () => {
|
|
renderer = TestRenderer.create(
|
|
<MemoryRouter initialEntries={["/install/sandbox"]}>
|
|
<Routes>
|
|
<Route path="/install/*" element={<InstallApp />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(renderTreeText(renderer!.toJSON())).toContain("Choose the sandbox runtime");
|
|
});
|
|
|
|
const daytonaButton = findOptionButton(renderer!, "Daytona");
|
|
await act(async () => {
|
|
daytonaButton.props.onClick();
|
|
});
|
|
|
|
const form = renderer!.root.findByType("form");
|
|
await act(async () => {
|
|
form.props.onSubmit({ preventDefault() {} });
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(renderTreeText(renderer!.toJSON())).toContain(
|
|
"Enter the Daytona API key before continuing.",
|
|
);
|
|
});
|
|
expect(fetchCalls.map((call) => String(call.input))).toEqual([
|
|
"/install/session",
|
|
]);
|
|
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
});
|
|
|
|
test("shows the GitHub App callback URL on the review step", async () => {
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
try {
|
|
const fetchMock = mock((input: RequestInfo | URL) => {
|
|
expect(String(input)).toBe("/install/session");
|
|
return Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server", "object_store", "llm", "github"],
|
|
llm: {
|
|
providers: [{ provider: "anthropic" }],
|
|
},
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: { provider: "local" },
|
|
github: {
|
|
strategy: "app",
|
|
owner: { kind: "personal" },
|
|
app_name: "octocat-fabro",
|
|
slug: "octocat-fabro",
|
|
allowed_username: "octocat",
|
|
},
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
},
|
|
),
|
|
);
|
|
});
|
|
useInstallFetchMock(fetchMock as typeof fetch);
|
|
|
|
const testWindow = createTestWindow("https://fabro.example.com/install/review");
|
|
testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
|
|
(globalThis as { window?: unknown }).window = testWindow;
|
|
|
|
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
await act(async () => {
|
|
renderer = TestRenderer.create(
|
|
<MemoryRouter initialEntries={["/install/review"]}>
|
|
<Routes>
|
|
<Route path="/install/*" element={<InstallApp />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
const text = renderTreeText(renderer!.toJSON());
|
|
expect(text).toContain("GitHub callback URL");
|
|
expect(text).toContain("https://fabro.example.com/auth/callback/github");
|
|
});
|
|
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
});
|
|
|
|
test("skips LLM setup with an empty providers list and advances to GitHub", async () => {
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
try {
|
|
const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
|
|
const fetchMock = mock((input: RequestInfo | URL, init?: RequestInit) => {
|
|
fetchCalls.push({ input, init });
|
|
if (String(input) === "/install/session" && fetchCalls.length === 1) {
|
|
return Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server", "object_store", "sandbox"],
|
|
llm: null,
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: { provider: "local" },
|
|
sandbox: { provider: "docker" },
|
|
github: null,
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
),
|
|
);
|
|
}
|
|
if (String(input) === "/install/llm") {
|
|
return Promise.resolve(new Response(null, { status: 204 }));
|
|
}
|
|
if (String(input) === "/install/session") {
|
|
return Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server", "object_store", "sandbox", "llm"],
|
|
llm: { providers: [] },
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: { provider: "local" },
|
|
sandbox: { provider: "docker" },
|
|
github: null,
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
),
|
|
);
|
|
}
|
|
throw new Error(`unexpected fetch: ${String(input)}`);
|
|
});
|
|
useInstallFetchMock(fetchMock as typeof fetch);
|
|
|
|
const testWindow = createTestWindow("https://fabro.example.com/install/llm");
|
|
testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
|
|
(globalThis as { window?: unknown }).window = testWindow;
|
|
|
|
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
await act(async () => {
|
|
renderer = TestRenderer.create(
|
|
<MemoryRouter initialEntries={["/install/llm"]}>
|
|
<Routes>
|
|
<Route path="/install/*" element={<InstallApp />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(renderTreeText(renderer!.toJSON())).toContain("Add your LLM credentials");
|
|
});
|
|
|
|
const skipButton = renderer!.root.findAll(
|
|
(node) => node.type === "button" && node.children.includes("Skip LLM setup"),
|
|
)[0];
|
|
expect(skipButton).toBeDefined();
|
|
await act(async () => {
|
|
skipButton!.props.onClick();
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(renderTreeText(renderer!.toJSON())).toContain("Connect GitHub");
|
|
});
|
|
|
|
const calls = fetchCalls.map((call) => String(call.input));
|
|
const putIdx = calls.indexOf("/install/llm");
|
|
expect(putIdx).toBeGreaterThanOrEqual(0);
|
|
expect(fetchCalls[putIdx]?.init?.body).toBe(JSON.stringify({ providers: [] }));
|
|
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
});
|
|
|
|
test("blocks Continue on the LLM step when no API keys are entered", async () => {
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
try {
|
|
const fetchMock = mock((input: RequestInfo | URL) => {
|
|
if (String(input) === "/install/session") {
|
|
return Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server", "object_store", "sandbox"],
|
|
llm: null,
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: { provider: "local" },
|
|
sandbox: { provider: "docker" },
|
|
github: null,
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
),
|
|
);
|
|
}
|
|
throw new Error(`unexpected fetch: ${String(input)}`);
|
|
});
|
|
useInstallFetchMock(fetchMock as typeof fetch);
|
|
|
|
const testWindow = createTestWindow("https://fabro.example.com/install/llm");
|
|
testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
|
|
(globalThis as { window?: unknown }).window = testWindow;
|
|
|
|
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
await act(async () => {
|
|
renderer = TestRenderer.create(
|
|
<MemoryRouter initialEntries={["/install/llm"]}>
|
|
<Routes>
|
|
<Route path="/install/*" element={<InstallApp />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(renderTreeText(renderer!.toJSON())).toContain("Add your LLM credentials");
|
|
});
|
|
|
|
const form = renderer!.root.findByType("form");
|
|
await act(async () => {
|
|
form.props.onSubmit({ preventDefault() {} });
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(renderTreeText(renderer!.toJSON())).toContain(
|
|
"Add at least one provider API key before continuing.",
|
|
);
|
|
});
|
|
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
});
|
|
|
|
test("shows a skipped LLM step as Skipped on the review step", async () => {
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
try {
|
|
const fetchMock = mock((input: RequestInfo | URL) => {
|
|
expect(String(input)).toBe("/install/session");
|
|
return Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server", "object_store", "sandbox", "llm", "github"],
|
|
llm: { providers: [] },
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: { provider: "local" },
|
|
sandbox: { provider: "docker" },
|
|
github: { strategy: "token", username: "octocat" },
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
),
|
|
);
|
|
});
|
|
useInstallFetchMock(fetchMock as typeof fetch);
|
|
|
|
const testWindow = createTestWindow("https://fabro.example.com/install/review");
|
|
testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
|
|
(globalThis as { window?: unknown }).window = testWindow;
|
|
|
|
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
await act(async () => {
|
|
renderer = TestRenderer.create(
|
|
<MemoryRouter initialEntries={["/install/review"]}>
|
|
<Routes>
|
|
<Route path="/install/*" element={<InstallApp />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
const text = renderTreeText(renderer!.toJSON());
|
|
expect(text).toContain("LLM providers");
|
|
expect(text).toContain("Skipped");
|
|
});
|
|
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
});
|
|
|
|
test("shows an incomplete LLM step as Not configured on the review step", async () => {
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
try {
|
|
const fetchMock = mock((input: RequestInfo | URL) => {
|
|
expect(String(input)).toBe("/install/session");
|
|
return Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server", "object_store", "sandbox", "github"],
|
|
llm: null,
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: { provider: "local" },
|
|
sandbox: { provider: "docker" },
|
|
github: { strategy: "token", username: "octocat" },
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
),
|
|
);
|
|
});
|
|
useInstallFetchMock(fetchMock as typeof fetch);
|
|
|
|
const testWindow = createTestWindow("https://fabro.example.com/install/review");
|
|
testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
|
|
(globalThis as { window?: unknown }).window = testWindow;
|
|
|
|
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
await act(async () => {
|
|
renderer = TestRenderer.create(
|
|
<MemoryRouter initialEntries={["/install/review"]}>
|
|
<Routes>
|
|
<Route path="/install/*" element={<InstallApp />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
const text = renderTreeText(renderer!.toJSON());
|
|
expect(text).toContain("LLM providers");
|
|
expect(text).toContain("Not configured");
|
|
});
|
|
expect(renderTreeText(renderer!.toJSON())).not.toContain("Skipped");
|
|
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
});
|
|
|
|
test("keeps the user on the LLM step when skipping fails", async () => {
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
try {
|
|
const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
|
|
const fetchMock = mock((input: RequestInfo | URL, init?: RequestInit) => {
|
|
fetchCalls.push({ input, init });
|
|
if (String(input) === "/install/session") {
|
|
return Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
completed_steps: ["server", "object_store", "sandbox"],
|
|
llm: null,
|
|
server: { canonical_url: "https://fabro.example.com" },
|
|
object_store: { provider: "local" },
|
|
sandbox: { provider: "docker" },
|
|
github: null,
|
|
prefill: INSTALL_PREFILL,
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
),
|
|
);
|
|
}
|
|
if (String(input) === "/install/llm") {
|
|
return Promise.resolve(new Response(null, { status: 500 }));
|
|
}
|
|
throw new Error(`unexpected fetch: ${String(input)}`);
|
|
});
|
|
useInstallFetchMock(fetchMock as typeof fetch);
|
|
|
|
const testWindow = createTestWindow("https://fabro.example.com/install/llm");
|
|
testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token");
|
|
(globalThis as { window?: unknown }).window = testWindow;
|
|
|
|
let renderer: TestRenderer.ReactTestRenderer | null = null;
|
|
await act(async () => {
|
|
renderer = TestRenderer.create(
|
|
<MemoryRouter initialEntries={["/install/llm"]}>
|
|
<Routes>
|
|
<Route path="/install/*" element={<InstallApp />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(renderTreeText(renderer!.toJSON())).toContain("Add your LLM credentials");
|
|
});
|
|
|
|
const skipButton = renderer!.root.findAll(
|
|
(node) => node.type === "button" && node.children.includes("Skip LLM setup"),
|
|
)[0];
|
|
await act(async () => {
|
|
skipButton!.props.onClick();
|
|
});
|
|
|
|
// The failed PUT must not advance to GitHub or refresh the session.
|
|
await waitFor(() => {
|
|
expect(fetchCalls.map((call) => String(call.input))).toContain("/install/llm");
|
|
});
|
|
const text = renderTreeText(renderer!.toJSON());
|
|
expect(text).toContain("Add your LLM credentials");
|
|
expect(text).not.toContain("Connect GitHub");
|
|
expect(
|
|
fetchCalls.filter((call) => String(call.input) === "/install/session"),
|
|
).toHaveLength(1);
|
|
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
});
|
|
});
|