// Copyright 2026 OpenObserve Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
import { describe, expect, it, vi } from "vitest";
import {
buildNavigationRoute,
generateNavigationFromToolResult,
navigationPageName,
} from "@/components/O2AIChat.navigation";
import type { NavigationAction } from "@/ts/interfaces/chat";
import { raw, type TranslateFn } from "@/types/i18n";
const t = ((key: string, params?: Record) =>
raw(params ? `${key}:${JSON.stringify(params)}` : key)) as unknown as TranslateFn;
const searchArgs = (over: Record = {}) => ({
stream_type: "logs",
request_body: { query: { sql: "SELECT * FROM default", start_time: 1, end_time: 2 } },
...over,
});
describe("generateNavigationFromToolResult", () => {
it("returns null without callArgs", () => {
expect(generateNavigationFromToolResult("Search", null, {}, t)).toBeNull();
});
it("builds a load_query action from a search tool", () => {
const action = generateNavigationFromToolResult("Search", searchArgs(), {}, t)!;
expect(action.action).toBe("load_query");
expect(action.resource_type).toBe("logs");
expect(action.target).toMatchObject({
query: "SELECT * FROM default",
sql_mode: true,
from: 1,
to: 2,
stream: ["default"],
});
});
it("prefers an explicit stream_name over the FROM clause", () => {
const action = generateNavigationFromToolResult(
"Search",
searchArgs({ stream_name: "a,b" }),
{},
t,
)!;
expect(action.target.stream).toEqual(["a", "b"]);
});
it("carries a VRL function through when present", () => {
const args = searchArgs();
args.request_body.query.functionContent = ".x = 1";
expect(generateNavigationFromToolResult("Search", args, {}, t)!.target.functionContent).toBe(
".x = 1",
);
});
it("returns null when the search has no time range", () => {
const args = searchArgs();
delete (args.request_body.query as any).end_time;
expect(generateNavigationFromToolResult("Search", args, {}, t)).toBeNull();
});
it("returns null when a search has sql but no derivable stream, never falling through", () => {
const args = searchArgs({
request_body: { query: { sql: "SELECT 1", start_time: 1, end_time: 2 } },
});
expect(generateNavigationFromToolResult("createAlert", args, { alert_id: "a1" }, t)).toBeNull();
});
it("titles unknown stream types", () => {
const action = generateNavigationFromToolResult(
"Search",
searchArgs({ stream_type: "enrichment" }),
{},
t,
)!;
expect(action.label).toContain("Enrichment");
});
it("returns null when the tool name matches no CRUD prefix", () => {
expect(generateNavigationFromToolResult("listThings", { a: 1 }, { id: "x" }, t)).toBeNull();
});
// A deleted resource has no page left to open, so a "View" link would lead to a missing record.
it("returns null for a delete tool even when the id is known", () => {
expect(
generateNavigationFromToolResult("deleteAlert", { alert_id: "a1" }, { alert_id: "a1" }, t),
).toBeNull();
expect(
generateNavigationFromToolResult("DeleteDashboard", { dashboard_id: "d1" }, {}, t),
).toBeNull();
});
it("still builds navigate_direct for get and update tools", () => {
expect(generateNavigationFromToolResult("getAlert", { alert_id: "a1" }, {}, t)?.action).toBe(
"navigate_direct",
);
expect(
generateNavigationFromToolResult("updateDashboard", { dashboard_id: "d1" }, {}, t)?.action,
).toBe("navigate_direct");
});
it("builds navigate_direct from a {resource}_id in the response", () => {
const action = generateNavigationFromToolResult(
"createAlert",
{ request_body: {} },
{ alert_id: "a1", name: "n" },
t,
)!;
expect(action.action).toBe("navigate_direct");
expect(action.resource_type).toBe("alert");
expect(action.target).toEqual({ alert_id: "a1", name: "n", folder: "default" });
});
it("accepts the camelCase id variant", () => {
const action = generateNavigationFromToolResult(
"getDashboard",
{ request_body: {} },
{ dashboardId: "d1" },
t,
)!;
expect(action.target.dashboard_id).toBe("d1");
});
it("omits folder for resources that do not use one", () => {
const action = generateNavigationFromToolResult(
"getPipeline",
{ request_body: {} },
{ pipeline_id: "p1" },
t,
)!;
expect(action.target.folder).toBeUndefined();
});
it("unwraps a JSON string response and its versioned payload", () => {
const action = generateNavigationFromToolResult(
"getDashboard",
{ request_body: {} },
{ response: JSON.stringify({ v5: { dashboard_id: "d5" } }) },
t,
)!;
expect(action.target.dashboard_id).toBe("d5");
});
it("unwraps the MCP content[0].text shape", () => {
const action = generateNavigationFromToolResult(
"getDashboard",
{ request_body: {} },
{ content: [{ text: JSON.stringify({ v8: { dashboard_id: "d8" } }) }] },
t,
)!;
expect(action.target.dashboard_id).toBe("d8");
});
it("returns null when the MCP text is unparseable", () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
expect(
generateNavigationFromToolResult(
"getDashboard",
{ request_body: {} },
{ content: [{ text: "not json" }] },
t,
),
).toBeNull();
warn.mockRestore();
});
it("lets callArgs.request_body override the parsed response", () => {
const action = generateNavigationFromToolResult(
"updateAlert",
{ alert_id: "fromArgs", request_body: { alert_id: "fromBody" } },
{ alert_id: "fromResponse" },
t,
)!;
expect(action.target.alert_id).toBe("fromBody");
});
});
describe("navigationPageName", () => {
it("prefers the label", () => {
expect(
navigationPageName({ label: raw("L"), resource_type: "alert", target: { name: "N" } } as any),
).toBe("L");
});
it("falls back to the target name, then the capitalised resource type", () => {
expect(
navigationPageName({ label: raw(""), resource_type: "alert", target: { name: "N" } } as any),
).toBe("N");
expect(navigationPageName({ label: raw(""), resource_type: "alert", target: {} } as any)).toBe(
"Alert",
);
});
});
describe("buildNavigationRoute", () => {
const decode = (s: string) => decodeURIComponent(escape(atob(s)));
it("builds the logs route for load_query", () => {
const action: NavigationAction = {
resource_type: "logs",
action: "load_query",
label: raw("l"),
target: { query: "SELECT 1", sql_mode: true, from: 1, to: 2, stream: ["a", "b"] },
};
const route = buildNavigationRoute(action, "org1")!;
expect(route.path).toBe("/logs");
expect(route.query).toMatchObject({
org_identifier: "org1",
stream_type: "logs",
stream: "a,b",
from: "1",
to: "2",
sql_mode: "true",
fn_editor: "false",
type: "ai_chat_query",
});
expect(decode(route.query.query)).toBe("SELECT 1");
});
it("uses period when no absolute range is given", () => {
const route = buildNavigationRoute(
{ resource_type: "logs", action: "load_query", label: raw("l"), target: { period: "15m" } },
"org1",
)!;
expect(route.query.period).toBe("15m");
expect(route.query.from).toBeUndefined();
});
it("enables the function editor when a VRL function is present", () => {
const route = buildNavigationRoute(
{
resource_type: "logs",
action: "load_query",
label: raw("l"),
target: { functionContent: ".x = 1" },
},
"org1",
)!;
expect(route.query.fn_editor).toBe("true");
expect(decode(route.query.functionContent)).toBe(".x = 1");
});
it("routes alerts to /alerts with the update action", () => {
const route = buildNavigationRoute(
{
resource_type: "alert",
action: "navigate_direct",
label: raw("l"),
target: { alert_id: "a1", name: "n" },
},
"org1",
)!;
expect(route.path).toBe("/alerts");
expect(route.query).toEqual({
org_identifier: "org1",
action: "update",
alert_id: "a1",
name: "n",
folder: "default",
});
});
it("routes dashboards to the view page with its fixed defaults", () => {
const route = buildNavigationRoute(
{
resource_type: "dashboard",
action: "navigate_direct",
label: raw("l"),
target: { dashboard_id: "d1" },
},
"org1",
)!;
expect(route.path).toBe("/dashboards/view");
expect(route.query).toMatchObject({
dashboard: "d1",
folder: "default",
tab: "tab-1",
refresh: "Off",
period: "15m",
print: "false",
});
});
it("routes pipelines to the edit page", () => {
const route = buildNavigationRoute(
{
resource_type: "pipeline",
action: "navigate_direct",
label: raw("l"),
target: { pipeline_id: "p1", name: "n" },
},
"org1",
)!;
expect(route.path).toBe("/pipeline/pipelines/edit");
expect(route.query).toMatchObject({ id: "p1", name: "n" });
});
it("falls back to /{resource_type} for an unknown navigate_direct resource", () => {
const route = buildNavigationRoute(
{ resource_type: "stream", action: "navigate_direct", label: raw("l"), target: {} },
"org1",
)!;
expect(route.path).toBe("/stream");
});
it("returns null for an action with no route", () => {
expect(
buildNavigationRoute(
{ resource_type: "alert", action: "noop" as any, label: raw("l"), target: {} },
"org1",
),
).toBeNull();
});
});