mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
227 lines
7.4 KiB
TypeScript
227 lines
7.4 KiB
TypeScript
// 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 <http://www.gnu.org/licenses/>.
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
formatContextKey,
|
|
formatContextValue,
|
|
formatToolCallMessage,
|
|
getToolCallDisplayData,
|
|
hasToolCallDetails,
|
|
splitAroundHighlight,
|
|
truncateQuery,
|
|
type ToolCallBlock,
|
|
} from "@/components/O2AIChat.toolcall";
|
|
import { raw, type TranslateFn } from "@/types/i18n";
|
|
|
|
// Mirrors the interpolating shape of t(): the param value ends up inside the sentence.
|
|
const t = ((key: string, params?: Record<string, any>) =>
|
|
raw(params ? `${key}[${Object.values(params).join("|")}]` : key)) as unknown as TranslateFn;
|
|
|
|
const block = (over: Partial<ToolCallBlock>): ToolCallBlock =>
|
|
({ type: "tool_call", ...over }) as ToolCallBlock;
|
|
|
|
describe("truncateQuery", () => {
|
|
it("returns empty for falsy input", () => {
|
|
expect(truncateQuery("")).toBe("");
|
|
});
|
|
|
|
it("keeps a query at exactly the limit", () => {
|
|
expect(truncateQuery("a".repeat(100))).toBe("a".repeat(100));
|
|
});
|
|
|
|
it("ellipsises past the limit", () => {
|
|
expect(truncateQuery("a".repeat(101))).toBe("a".repeat(100) + "...");
|
|
});
|
|
});
|
|
|
|
describe("formatContextKey", () => {
|
|
it("title-cases snake_case", () => {
|
|
expect(formatContextKey("start_time")).toBe("Start Time");
|
|
});
|
|
});
|
|
|
|
describe("formatContextValue", () => {
|
|
it("truncates strings over 30 chars", () => {
|
|
expect(formatContextValue("b".repeat(31))).toBe("b".repeat(30) + "...");
|
|
});
|
|
|
|
it("stringifies non-strings without truncating", () => {
|
|
const long = Array.from({ length: 40 }, (_, i) => i);
|
|
expect(formatContextValue(long)).toBe(long.join(","));
|
|
expect(formatContextValue(null)).toBe("null");
|
|
});
|
|
});
|
|
|
|
describe("getToolCallDisplayData", () => {
|
|
it("returns null without a context", () => {
|
|
expect(getToolCallDisplayData(null)).toBeNull();
|
|
});
|
|
|
|
it("returns null when nothing recognisable is present", () => {
|
|
expect(getToolCallDisplayData({ unrelated: 1 })).toBeNull();
|
|
});
|
|
|
|
it("pulls the nested search query fields", () => {
|
|
expect(
|
|
getToolCallDisplayData({
|
|
request_body: {
|
|
query: {
|
|
sql: "SELECT 1",
|
|
start_time: 1,
|
|
end_time: 2,
|
|
from: 0,
|
|
size: 10,
|
|
query_type: "sql",
|
|
vrl: ".a=1",
|
|
},
|
|
},
|
|
}),
|
|
).toEqual({
|
|
query: "SELECT 1",
|
|
start_time: 1,
|
|
end_time: 2,
|
|
from: 0,
|
|
size: 10,
|
|
query_type: "sql",
|
|
vrl: ".a=1",
|
|
});
|
|
});
|
|
|
|
it("lets request_body.function win over context.vrl", () => {
|
|
expect(getToolCallDisplayData({ vrl: "old", request_body: { function: "new" } })!.vrl).toBe(
|
|
"new",
|
|
);
|
|
});
|
|
|
|
it("reads the flat shapes", () => {
|
|
expect(
|
|
getToolCallDisplayData({ sql: "S", stream_name: "n", type: "logs", command: "ls" }),
|
|
).toEqual({ query: "S", stream: "n", type: "logs", command: "ls" });
|
|
});
|
|
});
|
|
|
|
describe("hasToolCallDetails", () => {
|
|
it("is true for a failure, a summary, or a response", () => {
|
|
expect(hasToolCallDetails(block({ success: false }))).toBe(true);
|
|
expect(hasToolCallDetails(block({ summary: { count: 0 } as any }))).toBe(true);
|
|
expect(hasToolCallDetails(block({ response: {} }))).toBe(true);
|
|
});
|
|
|
|
it("falls back to whether the context has display data", () => {
|
|
expect(hasToolCallDetails(block({ context: { stream_name: "n" } }))).toBe(true);
|
|
expect(hasToolCallDetails(block({ context: {} }))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("splitAroundHighlight", () => {
|
|
it("splits around the highlight", () => {
|
|
expect(splitAroundHighlight("see foo now", "foo")).toEqual({
|
|
text: "see ",
|
|
highlight: "foo",
|
|
suffix: " now",
|
|
});
|
|
});
|
|
|
|
it("returns the whole sentence when the highlight is absent", () => {
|
|
expect(splitAroundHighlight("see now", "foo")).toEqual({
|
|
text: "see now",
|
|
highlight: null,
|
|
suffix: "",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("formatToolCallMessage", () => {
|
|
it("reports VRL validation outcomes", () => {
|
|
expect(formatToolCallMessage(block({ tool: "testFunction", success: false }), t).text).toBe(
|
|
"aiAssistant.aiChat.toolVrlValidationFailed",
|
|
);
|
|
expect(formatToolCallMessage(block({ tool: "testFunction" }), t).text).toBe(
|
|
"aiAssistant.aiChat.toolVrlValidated",
|
|
);
|
|
});
|
|
|
|
it("reports a failed SearchSQL", () => {
|
|
expect(formatToolCallMessage(block({ tool: "SearchSQL", success: false }), t).text).toBe(
|
|
"aiAssistant.aiChat.toolQueryFailed",
|
|
);
|
|
});
|
|
|
|
it("reports SearchSQL hit counts with the stream type", () => {
|
|
const out = formatToolCallMessage(
|
|
block({ tool: "SearchSQL", response: { total: 7 }, context: { type: "traces" } }),
|
|
t,
|
|
);
|
|
expect(out.text).toBe("aiAssistant.aiChat.toolQueriedStream[traces]");
|
|
expect(out.highlight).toBe("aiAssistant.aiChat.toolResultsCount[7]");
|
|
});
|
|
|
|
it("defaults the SearchSQL stream type to logs", () => {
|
|
expect(
|
|
formatToolCallMessage(block({ tool: "SearchSQL", response: { total: 0 } }), t).text,
|
|
).toBe("aiAssistant.aiChat.toolQueriedStream[logs]");
|
|
});
|
|
|
|
it("highlights the id in the Get* tool sentences", () => {
|
|
expect(
|
|
formatToolCallMessage(block({ tool: "GetAlert", context: { alert_id: "a1" } }), t).highlight,
|
|
).toBe("a1");
|
|
expect(
|
|
formatToolCallMessage(block({ tool: "GetIncident", context: { incident_id: "i1" } }), t)
|
|
.highlight,
|
|
).toBe("i1");
|
|
expect(
|
|
formatToolCallMessage(block({ tool: "GetDashboard", context: { dashboard_id: "d1" } }), t)
|
|
.highlight,
|
|
).toBe("d1");
|
|
expect(
|
|
formatToolCallMessage(block({ tool: "StreamSchema", context: { stream_name: "s1" } }), t)
|
|
.highlight,
|
|
).toBe("s1");
|
|
});
|
|
|
|
it("appends a found-count for list tools", () => {
|
|
const out = formatToolCallMessage(
|
|
block({ tool: "ListAlerts", message: raw("Listing"), response: { total: 3 } }),
|
|
t,
|
|
);
|
|
expect(out.text).toBe("Listing ");
|
|
expect(out.highlight).toBe("aiAssistant.aiChat.toolFoundCount[3]");
|
|
});
|
|
|
|
it("falls back to the tool name when a list tool has no message", () => {
|
|
expect(
|
|
formatToolCallMessage(block({ tool: "ListAlerts", response: { total: 3 } }), t).text,
|
|
).toBe("ListAlerts ");
|
|
});
|
|
|
|
it("truncates a long failure message", () => {
|
|
const out = formatToolCallMessage(block({ success: false, resultMessage: "e".repeat(61) }), t);
|
|
expect(out.text).toBe("e".repeat(60) + "...");
|
|
});
|
|
|
|
it("appends a results count from the summary", () => {
|
|
const out = formatToolCallMessage(block({ tool: "Whatever", summary: { count: 2 } as any }), t);
|
|
expect(out.text).toBe("Whatever ");
|
|
expect(out.highlight).toBe("aiAssistant.aiChat.toolResultsCount[2]");
|
|
});
|
|
|
|
it("falls back to the block message", () => {
|
|
expect(formatToolCallMessage(block({ message: raw("plain") }), t).text).toBe("plain");
|
|
});
|
|
});
|