mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 07:42:13 +03:00
fix(dashboard): surface manual config CTA when Open Claw CLI auto-detect fails (#4562)
Integrated into release/v3.8.34 (rebuilt onto tip)
This commit is contained in:
committed by
GitHub
parent
d6caf6ed39
commit
be2a38818e
@@ -339,6 +339,20 @@ export default function OpenClawToolCard({
|
||||
: t("installCliPrompt", { tool: "Open Claw" })}
|
||||
</p>
|
||||
</div>
|
||||
{/*
|
||||
Always surface Manual Config even when the CLI is not
|
||||
detected locally — typical of remote OmniRoute
|
||||
deployments where the CLI lives on the user's machine,
|
||||
not on the server. Upstream report: #579.
|
||||
*/}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowManualConfigModal(true)}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">content_copy</span>
|
||||
{t("manualConfig")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
160
tests/unit/ui/OpenClawToolCard-manual-config-fallback.test.tsx
Normal file
160
tests/unit/ui/OpenClawToolCard-manual-config-fallback.test.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// Regression test: when the Open Claw CLI is not detected locally (typical of
|
||||
// remote OmniRoute deployments where the CLI lives on the user's laptop, not
|
||||
// on the server), the card must still surface a "Manual Config" button so the
|
||||
// user can copy the settings.json snippet and paste it into the CLI on their
|
||||
// local machine. Before this fix the Manual Config button only rendered when
|
||||
// `cliReady === true`, which made the card useless for remote deployments
|
||||
// (upstream report: decolua/9router#579, port of decolua/9router#615).
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// ── Mocks ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: () => (key: string, values?: Record<string, unknown>) => {
|
||||
const messages: Record<string, string> = {
|
||||
cliNotInstalled: "{tool} CLI not detected locally",
|
||||
cliNotRunnable: "{tool} CLI installed but not runnable",
|
||||
installCliPrompt:
|
||||
"Manual configuration is still available if OmniRoute is deployed on a remote server.",
|
||||
cliFoundFailedHealthcheck: "{tool} CLI was found but failed runtime healthcheck{reason}.",
|
||||
manualConfig: "Manual Config",
|
||||
checkingCli: "Checking {tool}...",
|
||||
openClawManualConfiguration: "Open Claw Manual Configuration",
|
||||
"toolDescriptions.openclaw": "Open Claw CLI",
|
||||
};
|
||||
const raw = messages[key] ?? key;
|
||||
if (!values) return raw;
|
||||
return Object.entries(values).reduce(
|
||||
(acc, [k, v]) => acc.replaceAll(`{${k}}`, String(v ?? "")),
|
||||
raw
|
||||
);
|
||||
},
|
||||
useLocale: () => "en",
|
||||
}));
|
||||
|
||||
vi.mock("next/image", () => ({
|
||||
default: () => <span data-testid="next-image" />,
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/dashboard/cli-code/components/CliStatusBadge", () => ({
|
||||
default: () => <span data-testid="status-badge" />,
|
||||
}));
|
||||
|
||||
// Surface ManualConfigModal as a marker so we can assert it gets rendered with
|
||||
// isOpen=true after clicking the new Manual Config button.
|
||||
vi.mock("@/shared/components", async () => {
|
||||
const React = await import("react");
|
||||
return {
|
||||
Card: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}) => (
|
||||
<button type="button" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
ModelSelectModal: () => null,
|
||||
ManualConfigModal: ({ isOpen, title }: { isOpen: boolean; title?: string }) =>
|
||||
isOpen ? <div data-testid="manual-config-modal">{title}</div> : null,
|
||||
};
|
||||
});
|
||||
|
||||
// ── Fetch stub ────────────────────────────────────────────────────────────────
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
// Return: CLI not installed (the broken-on-remote case from upstream #579).
|
||||
globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === "string" ? input : input.toString();
|
||||
if (url.includes("/api/cli-tools/openclaw-settings")) {
|
||||
return new Response(JSON.stringify({ installed: false }), { status: 200 });
|
||||
}
|
||||
if (url.includes("/api/models/alias")) {
|
||||
return new Response(JSON.stringify({ aliases: {} }), { status: 200 });
|
||||
}
|
||||
if (url.includes("/api/cli-tools/backups")) {
|
||||
return new Response(JSON.stringify({ backups: [] }), { status: 200 });
|
||||
}
|
||||
return new Response("{}", { status: 200 });
|
||||
}) as unknown as typeof fetch;
|
||||
});
|
||||
|
||||
const containers: HTMLElement[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
while (containers.length > 0) {
|
||||
containers.pop()?.remove();
|
||||
}
|
||||
document.body.innerHTML = "";
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
// ── Import under test (after mocks) ───────────────────────────────────────────
|
||||
|
||||
const { default: OpenClawToolCard } = await import(
|
||||
"@/app/(dashboard)/dashboard/cli-code/components/OpenClawToolCard"
|
||||
);
|
||||
|
||||
async function renderExpanded() {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
containers.push(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<OpenClawToolCard
|
||||
tool={{ name: "Open Claw", defaultModels: [] }}
|
||||
isExpanded={true}
|
||||
onToggle={() => {}}
|
||||
activeProviders={[]}
|
||||
baseUrl="http://localhost:20128"
|
||||
hasActiveProviders={false}
|
||||
apiKeys={[]}
|
||||
cloudEnabled={false}
|
||||
batchStatus={null}
|
||||
lastConfiguredAt={null}
|
||||
/>
|
||||
);
|
||||
});
|
||||
// Allow microtasks for the fetch() promise + state update to flush.
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
describe("OpenClawToolCard — manual-config CTA when CLI is not detected", () => {
|
||||
it("renders the Manual Config button when CLI is not installed", async () => {
|
||||
const container = await renderExpanded();
|
||||
const buttons = Array.from(container.querySelectorAll("button"));
|
||||
const labels = buttons.map((b) => b.textContent ?? "");
|
||||
expect(labels.some((l) => l.includes("Manual Config"))).toBe(true);
|
||||
});
|
||||
|
||||
it("opens the ManualConfigModal when the Manual Config button is clicked", async () => {
|
||||
const container = await renderExpanded();
|
||||
const manualBtn = Array.from(container.querySelectorAll("button")).find((b) =>
|
||||
(b.textContent ?? "").includes("Manual Config")
|
||||
);
|
||||
expect(manualBtn).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
manualBtn!.click();
|
||||
});
|
||||
|
||||
expect(container.querySelector("[data-testid='manual-config-modal']")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user