// 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 } from "vitest";
import { raw, type TranslateFn } from "@/types/i18n";
import {
chatErrorMessage,
createPreview,
filterMarkdownHeaders,
formatLogEntryContent,
getLanguageDisplay,
parseLogEntries,
processHtmlBlock,
processMessageContent,
processTextBlock,
renderMarkdown,
} from "@/components/O2AIChat.content";
describe("filterMarkdownHeaders", () => {
it("turns h1 and h2 into bold-with-colon", () => {
expect(filterMarkdownHeaders("# One\n## Two")).toBe("**One:**\n**Two:**");
});
it("leaves headers inside fenced code blocks alone", () => {
const input = "# Real\n```\n# comment\n```\n";
expect(filterMarkdownHeaders(input)).toBe("**Real:**\n```\n# comment\n```\n");
});
it("restores several code blocks in order", () => {
const input = "```a```\n# H\n```b```";
expect(filterMarkdownHeaders(input)).toBe("```a```\n**H:**\n```b```");
});
it("leaves h3 and deeper untouched", () => {
expect(filterMarkdownHeaders("### Three")).toBe("### Three");
});
});
describe("processTextBlock", () => {
it("is the same function as processMessageContent", () => {
expect(processMessageContent).toBe(processTextBlock);
});
it("returns a text block for prose", () => {
const blocks = processTextBlock("hello");
expect(blocks).toHaveLength(1);
expect(blocks[0].type).toBe("text");
expect(blocks[0].content).toContain("hello");
});
it("returns a highlighted code block with its language", () => {
const blocks = processTextBlock("```sql\nSELECT 1\n```");
expect(blocks[0].type).toBe("code");
expect(blocks[0].language).toBe("sql");
expect(blocks[0].content).toBe("SELECT 1");
expect(blocks[0].highlightedContent).toContain("hljs-keyword");
});
// Stripping them removed real code, and the copy button then copied a broken snippet.
it("keeps leading comment lines in a code block", () => {
expect(processTextBlock("```sql\n-- a note\nSELECT 1\n```")[0].content).toBe(
"-- a note\nSELECT 1",
);
expect(processTextBlock("```bash\n#!/bin/bash\necho hi\n```")[0].content).toBe(
"#!/bin/bash\necho hi",
);
expect(processTextBlock("```js\n// setup\nconst a = 1;\n```")[0].content).toBe(
"// setup\nconst a = 1;",
);
});
it("still highlights a code block with an unknown language", () => {
const blocks = processTextBlock("```notalang\nx = 1\n```");
expect(blocks[0].language).toBe("notalang");
expect(blocks[0].highlightedContent).toBeTypeOf("string");
});
it("interleaves text and code in source order", () => {
const blocks = processTextBlock("before\n\n```js\nvar a = 1\n```\n\nafter");
const codeAt = blocks.findIndex((b) => b.type === "code");
expect(codeAt).toBeGreaterThan(0);
expect(blocks[0].content).toContain("before");
expect(blocks[blocks.length - 1].content).toContain("after");
});
it("applies the header filter before lexing", () => {
expect(processTextBlock("# Title")[0].content).toContain("");
});
});
describe("renderMarkdown", () => {
it("honours the gfm line-break option set by the module", () => {
expect(renderMarkdown("a\nb")).toContain(" ");
});
});
describe("formatLogEntryContent", () => {
it("pretty-prints and class-tags JSON", () => {
const html = formatLogEntryContent('{"a":"x","b":1,"c":true,"d":null}');
expect(html).toContain('"a":');
expect(html).toContain('"x"');
expect(html).toContain('1');
expect(html).toContain('true');
expect(html).toContain('null');
});
it("escapes non-JSON text and converts newlines", () => {
expect(formatLogEntryContent("&x\nnext")).toBe("<b>&x</b> next");
});
});
describe("createPreview", () => {
it("collapses whitespace in plain text", () => {
expect(createPreview("a \n b")).toBe("a b");
});
it("truncates past the max length", () => {
expect(createPreview("x".repeat(50), 10)).toBe("x".repeat(10) + "...");
});
it("summarises the first three keys of a JSON object", () => {
expect(createPreview('{"a":"1","b":2,"c":3,"d":4}', 100)).toBe('{a: "1", b: 2, c: 3, ...}');
});
it("truncates long string values inside the key summary", () => {
expect(createPreview('{"a":"0123456789abc"}', 100)).toBe('{a: "01234567..."}');
});
it("falls back to plain text for a JSON scalar", () => {
expect(createPreview("123")).toBe("123");
});
});
describe("parseLogEntries", () => {
it("returns the whole message as one text block when there are no delimiters", () => {
expect(parseLogEntries("just text")).toEqual([{ type: "text", text: "just text" }]);
});
it("returns nothing for empty content", () => {
expect(parseLogEntries("")).toEqual([]);
});
it("splits text around a log entry and keeps order", () => {
const blocks = parseLogEntries("before\n--- f.log ---\nbody\n--- end ---\nafter");
expect(blocks.map((b) => b.type)).toEqual(["text", "log_entry", "text"]);
expect(blocks[0].text).toBe("before");
expect(blocks[1]).toMatchObject({ filename: "f.log", content: "body", preview: "body" });
expect(blocks[1].lineStart).toBeUndefined();
expect(blocks[2].text).toBe("after");
});
it("captures the optional line range", () => {
const blocks = parseLogEntries("--- f.log (lines 3-9) ---\nbody\n--- end ---");
expect(blocks[0]).toMatchObject({ lineStart: 3, lineEnd: 9 });
});
it("parses several entries in one message", () => {
const one = "--- a ---\nA\n--- end ---";
const two = "--- b ---\nB\n--- end ---";
expect(parseLogEntries(`${one}\n${two}`).map((b) => b.type)).toEqual([
"log_entry",
"log_entry",
]);
});
});
describe("getLanguageDisplay", () => {
it("maps known aliases case-insensitively", () => {
expect(getLanguageDisplay("JS")).toBe("JavaScript");
expect(getLanguageDisplay("yml")).toBe("YAML");
});
it("upper-cases anything unmapped", () => {
expect(getLanguageDisplay("rust")).toBe("RUST");
});
});
describe("chatErrorMessage", () => {
const t = ((key: string) => raw(`t:${key}`)) as unknown as TranslateFn;
const err = (message: string | undefined, status?: number) =>
Object.assign(new Error(message), status === undefined ? {} : { status });
it("maps status 403 to the unauthorized key, even when a message is present", () => {
expect(chatErrorMessage(err("forbidden body"), t)).toBe("forbidden body");
expect(chatErrorMessage(err("forbidden body", 403), t)).toBe("t:common.unauthorizedAccess");
expect(chatErrorMessage({ status: 403 }, t)).toBe("t:common.unauthorizedAccess");
});
it("does not treat a string status of 403 as unauthorized", () => {
expect(chatErrorMessage({ status: "403", message: "m" }, t)).toBe("m");
});
it("uses any other message verbatim, whatever the status", () => {
expect(chatErrorMessage(err("Server error 500", 500), t)).toBe("Server error 500");
});
it("falls back to the generic key for the no-body sentinel or an empty message", () => {
expect(chatErrorMessage(err("No response body"), t)).toBe(
"t:aiAssistant.aiChat.serverResponseError",
);
expect(chatErrorMessage(err(""), t)).toBe("t:aiAssistant.aiChat.serverResponseError");
expect(chatErrorMessage({}, t)).toBe("t:aiAssistant.aiChat.serverResponseError");
});
it("throws on a nullish error, as the inline catch did", () => {
expect(() => chatErrorMessage(undefined, t)).toThrow(TypeError);
expect(() => chatErrorMessage(null, t)).toThrow(TypeError);
});
});
describe("processHtmlBlock", () => {
it("swaps pre for the generated-code-block span, keeping attributes", () => {
expect(processHtmlBlock('
y
')).toBe(
'y',
);
});
it("strips script tags", () => {
expect(processHtmlBlock("ok")).toBe("ok");
});
it("keeps the default profile for the live chat", () => {
expect(processHtmlBlock('')).toContain("x.example");
});
describe("strict (read-only) profile", () => {
it("drops remote images but keeps inline ones", () => {
const html = processHtmlBlock(
'',
true,
);
expect(html).not.toContain("x.example");
expect(html).toContain("data:image/png;base64,AAAA");
});
it("removes styles and style attributes", () => {
const html = processHtmlBlock(
'
hi
',
true,
);
expect(html).toBe("
hi
");
});
it("hardens links", () => {
const html = processHtmlBlock('d', true);
expect(html).toContain('rel="noopener noreferrer nofollow"');
expect(html).toContain('target="_blank"');
});
it("drops svg and media that could load remote content", () => {
const html = processHtmlBlock(
'ok',
true,
);
expect(html).toBe("ok");
});
});
});