mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 14:58:39 +08:00
* Add partial stack modes to agent-canvas Co-authored-by: openhands <openhands@all-hands.dev> * Simplify backend registry selection Co-authored-by: openhands <openhands@all-hands.dev> * Fix backend selection CI regressions * Seed local proxy backend in cloud tests * Stabilize onboarding snapshot navigation * Stabilize ingress tests on Windows * Remove local backend fallback for cloud calls * Fix frontend-only backend proxy target * Test backend-only launch without build * Add pending workflow and status updates * Use active LLM profile for setup banner * Show backend connection errors before saving * Validate backend keys in health checks * Update backend selector health test mock * Fix frontend-only workspace path * Preserve OpenHands proxy base URL * Address backend review comments * Preserve profile config when switching models * Fail fast on profile export errors * Throw AgentServerUnavailableError when backend registry is empty When no backend is configured (empty registry / NO_BACKEND sentinel), loadAgentServerInfo() was returning null without throwing, causing OptionService.getConfig() to succeed silently. root.tsx then rendered the home page instead of the MissingAgentServerScreen with the manage backends modal. Now loadAgentServerInfo() checks for the NO_BACKEND sentinel when getEffectiveLocalBackend() returns null and throws AgentServerUnavailableError, which root.tsx already handles by showing the manage backends modal. The cloud-backend path (also null from getEffectiveLocalBackend) is preserved — it still returns null. Co-authored-by: openhands <openhands@all-hands.dev> * chore: Remove PR-only artifacts --------- Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
131 lines
3.8 KiB
TypeScript
131 lines
3.8 KiB
TypeScript
import { afterAll, afterEach, beforeAll, beforeEach, vi } from "vitest";
|
|
import { cleanup } from "@testing-library/react";
|
|
import { server } from "#/mocks/node";
|
|
import "@testing-library/jest-dom/vitest";
|
|
|
|
// Some modules read env at import time before Vitest's per-test hooks run.
|
|
// The beforeEach below restores the same default after tests call
|
|
// `vi.unstubAllEnvs()`.
|
|
vi.stubEnv("VITE_SESSION_API_KEY", "test-session-key");
|
|
|
|
if (typeof HTMLCanvasElement !== "undefined") {
|
|
HTMLCanvasElement.prototype.getContext = vi.fn();
|
|
}
|
|
|
|
if (typeof HTMLElement !== "undefined") {
|
|
HTMLElement.prototype.scrollTo = vi.fn();
|
|
}
|
|
|
|
const windowStub =
|
|
typeof window === "undefined"
|
|
? ({ event: undefined } as unknown as Window & typeof globalThis)
|
|
: window;
|
|
|
|
vi.stubGlobal("window", windowStub);
|
|
windowStub.scrollTo = vi.fn();
|
|
|
|
// Node.js 25+ ships a built-in localStorage that requires --localstorage-file
|
|
// and is not functional without it. Stub it with a plain in-memory
|
|
// implementation so zustand's persist middleware works in tests.
|
|
if (
|
|
typeof localStorage === "undefined" ||
|
|
typeof localStorage.setItem !== "function"
|
|
) {
|
|
const store: Record<string, string> = {};
|
|
vi.stubGlobal("localStorage", {
|
|
getItem: (key: string) => store[key] ?? null,
|
|
setItem: (key: string, value: string) => {
|
|
store[key] = String(value);
|
|
},
|
|
removeItem: (key: string) => {
|
|
delete store[key];
|
|
},
|
|
clear: () => {
|
|
Object.keys(store).forEach((k) => delete store[k]);
|
|
},
|
|
get length() {
|
|
return Object.keys(store).length;
|
|
},
|
|
key: (index: number) => Object.keys(store)[index] ?? null,
|
|
});
|
|
}
|
|
|
|
if (typeof requestAnimationFrame === "undefined") {
|
|
vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) =>
|
|
setTimeout(() => callback(0), 0),
|
|
);
|
|
vi.stubGlobal(
|
|
"cancelAnimationFrame",
|
|
(timeoutId: ReturnType<typeof setTimeout>) => clearTimeout(timeoutId),
|
|
);
|
|
}
|
|
|
|
// Mock ResizeObserver for test environment
|
|
class MockResizeObserver {
|
|
observe = vi.fn();
|
|
|
|
unobserve = vi.fn();
|
|
|
|
disconnect = vi.fn();
|
|
}
|
|
|
|
// Mock the i18n provider
|
|
vi.mock("react-i18next", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("react-i18next")>()),
|
|
useTranslation: () => ({
|
|
t: (key: string) => key,
|
|
i18n: {
|
|
language: "en",
|
|
exists: () => false,
|
|
},
|
|
}),
|
|
}));
|
|
|
|
vi.mock("#/hooks/use-is-on-tos-page", () => ({
|
|
useIsOnTosPage: () => false,
|
|
}));
|
|
|
|
vi.mock("#/hooks/use-is-on-intermediate-page", () => ({
|
|
useIsOnIntermediatePage: () => false,
|
|
}));
|
|
|
|
// Mock useRevalidator from react-router to allow direct store manipulation in tests
|
|
vi.mock("react-router", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("react-router")>()),
|
|
useRevalidator: () => ({
|
|
revalidate: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
// Import the Zustand mock to enable automatic store resets
|
|
vi.mock("zustand");
|
|
|
|
// Mock requests during tests
|
|
beforeAll(() => {
|
|
server.listen({ onUnhandledRequest: "bypass" });
|
|
vi.stubGlobal("ResizeObserver", MockResizeObserver);
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.stubEnv("VITE_SESSION_API_KEY", "test-session-key");
|
|
});
|
|
|
|
afterEach(async () => {
|
|
server.resetHandlers();
|
|
// Cleanup the document body after each test
|
|
cleanup();
|
|
// Drain any queued microtasks before jsdom is torn down between test files.
|
|
// Without this, async state updates queued during render (for example by
|
|
// HeroUI v2 components wrapped in framer-motion's LazyMotion) can resolve
|
|
// after `window` is gone and trigger spurious unhandled rejections in
|
|
// react-dom's `resolveUpdatePriority`. We use `Promise.resolve()` (a
|
|
// microtask) rather than `setTimeout(0)` so this stays compatible with
|
|
// tests that install fake timers.
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
afterAll(() => {
|
|
server.close();
|
|
vi.unstubAllGlobals();
|
|
});
|