File size: 3,124 Bytes
c0af099
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen } from "@testing-library/react";
import { createMemoryRouter, RouterProvider } from "react-router";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
  setActiveSelection,
  setRegisteredBackends,
} from "#/api/backend-registry/active-store";
import {
  __resetHealthStoreForTests,
  recordBackendFailure,
} from "#/api/backend-registry/health-store";
import { MAX_CONSECUTIVE_FAILURES } from "#/api/backend-registry/health-storage";
import { CLOUD_BACKEND_API_KEY_OR_NETWORK_ERROR } from "#/hooks/query/use-backends-health";
import type { Backend } from "#/api/backend-registry/types";
import { ActiveBackendProvider } from "#/contexts/active-backend-context";
import { ONBOARDING_COMPLETED_STORAGE_KEY } from "#/components/features/onboarding/use-onboarding-completion";
import App from "#/root";

// The recovery screen lazy-loads the Manage Backends modal; stub it so the test
// asserts the routing decision rather than the modal's internals.
vi.mock("#/components/features/backends/manage-backends-modal", () => ({
  ManageBackendsModal: () => <div data-testid="manage-backends-modal" />,
}));

const cloudBackend: Backend = {
  id: "cloud-ohe",
  name: "Adorable Enterprise",
  host: "https://app.adorable.build.one",
  apiKey: "oh-cloud-key",
  kind: "cloud",
};

function renderApp() {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  const router = createMemoryRouter(
    [
      {
        path: "/",
        Component: App,
        children: [
          { index: true, element: <div data-testid="app-outlet-content" /> },
        ],
      },
    ],
    { initialEntries: ["/"] },
  );

  return render(
    <QueryClientProvider client={queryClient}>
      <ActiveBackendProvider>
        <RouterProvider router={router} />
      </ActiveBackendProvider>
    </QueryClientProvider>,
  );
}

describe("App root — active cloud backend connectivity gate", () => {
  beforeEach(() => {
    localStorage.clear();
    __resetHealthStoreForTests();
    localStorage.setItem(ONBOARDING_COMPLETED_STORAGE_KEY, "1");
    setRegisteredBackends([cloudBackend]);
    setActiveSelection({ backendId: cloudBackend.id });
  });

  afterEach(() => {
    setActiveSelection(null);
    setRegisteredBackends([]);
    localStorage.clear();
    __resetHealthStoreForTests();
  });

  it("shows the backend recovery screen when the active cloud backend is unreachable (CORS/network)", async () => {
    // Emulate a self-hosted OHE that doesn't allow this frontend's origin:
    // repeated CORS/network probe failures until the backend is disabled.
    for (let i = 0; i < MAX_CONSECUTIVE_FAILURES; i += 1) {
      recordBackendFailure(
        cloudBackend.id,
        new Error(CLOUD_BACKEND_API_KEY_OR_NETWORK_ERROR),
      );
    }

    renderApp();

    expect(
      await screen.findByTestId("agent-server-onboarding-screen"),
    ).toBeInTheDocument();
    expect(screen.queryByTestId("app-outlet-content")).not.toBeInTheDocument();
  });
});