fabro/apps/fabro-web/app/install-app.test.tsx
Bryan Helmkamp 160f587a1d
feat(install): enable only allowed sandbox providers
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>
2026-06-02 18:39:31 -04:00

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;
}
});
});