Files
bifrost/ui/lib/message/variables.test.ts
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

363 lines
13 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { Message } from "./message";
import {
extractVariablesFromMessages,
extractVariablesFromText,
mergeVariables,
replaceVariablesInMessages,
replaceVariablesInText,
} from "./variables";
// =============================================================================
// extractVariablesFromText
// =============================================================================
describe("extractVariablesFromText", () => {
it("extracts a single variable", () => {
expect(extractVariablesFromText("Hello {{ name }}")).toEqual(["name"]);
});
it("extracts multiple distinct variables", () => {
expect(extractVariablesFromText("{{ greeting }}, {{ name }}!")).toEqual(["greeting", "name"]);
});
it("deduplicates repeated variables", () => {
expect(extractVariablesFromText("{{ x }} and {{ x }}")).toEqual(["x"]);
});
it("returns empty array when no variables exist", () => {
expect(extractVariablesFromText("Hello world")).toEqual([]);
});
it("returns empty array for empty string", () => {
expect(extractVariablesFromText("")).toEqual([]);
});
it("handles variable with no spaces inside braces", () => {
expect(extractVariablesFromText("{{name}}")).toEqual(["name"]);
});
it("handles variable with extra whitespace inside braces", () => {
expect(extractVariablesFromText("{{ name }}")).toEqual(["name"]);
});
it("handles underscored variable names", () => {
expect(extractVariablesFromText("{{ user_name }}")).toEqual(["user_name"]);
});
it("handles dot-notation variable names", () => {
expect(extractVariablesFromText("{{ user.name }}")).toEqual(["user.name"]);
});
it("handles variables with numbers in name", () => {
expect(extractVariablesFromText("{{ item1 }} {{ item2 }}")).toEqual(["item1", "item2"]);
});
it("handles variable starting with underscore", () => {
expect(extractVariablesFromText("{{ _private }}")).toEqual(["_private"]);
});
it("does not extract variables starting with a number", () => {
expect(extractVariablesFromText("{{ 1abc }}")).toEqual([]);
});
it("does not extract variables with special characters", () => {
expect(extractVariablesFromText("{{ na-me }}")).toEqual([]);
});
it("handles multiline text with variables", () => {
const text = `Line one {{ first }}
Line two {{ second }}
Line three`;
expect(extractVariablesFromText(text)).toEqual(["first", "second"]);
});
it("ignores jinja2 block tags", () => {
expect(extractVariablesFromText("{% if condition %}yes{% endif %}")).toEqual([]);
});
it("ignores jinja2 comments", () => {
expect(extractVariablesFromText("{# this is a comment #}")).toEqual([]);
});
it("extracts variables adjacent to jinja2 block tags", () => {
const text = "{% if show %}{{ name }}{% endif %}";
expect(extractVariablesFromText(text)).toEqual(["name"]);
});
it("handles triple braces (not valid jinja2) gracefully", () => {
// {{{ name }}} — the regex should still find "name" from the inner {{ }}
const result = extractVariablesFromText("{{{ name }}}");
expect(result).toEqual(["name"]);
});
it("handles variables embedded in longer text", () => {
const text = "Dear {{ title }} {{ last_name }}, your order #{{ order_id }} is ready.";
expect(extractVariablesFromText(text)).toEqual(["title", "last_name", "order_id"]);
});
// Regression: calling extractVariablesFromText consecutively should work
// (ensures regex lastIndex is properly reset)
it("works correctly when called multiple times in succession", () => {
expect(extractVariablesFromText("{{ a }}")).toEqual(["a"]);
expect(extractVariablesFromText("{{ b }}")).toEqual(["b"]);
expect(extractVariablesFromText("{{ a }} {{ c }}")).toEqual(["a", "c"]);
});
});
// =============================================================================
// extractVariablesFromMessages
// =============================================================================
describe("extractVariablesFromMessages", () => {
it("extracts variables from a system message", () => {
const messages = [Message.system("You are {{ role }}")];
expect(extractVariablesFromMessages(messages)).toEqual(["role"]);
});
it("extracts variables from a user message", () => {
const messages = [Message.request("Tell me about {{ topic }}")];
expect(extractVariablesFromMessages(messages)).toEqual(["topic"]);
});
it("extracts variables from an assistant message", () => {
const messages = [Message.response("Hello {{ name }}")];
expect(extractVariablesFromMessages(messages)).toEqual([]);
});
it("extracts variables across multiple messages", () => {
const messages = [
Message.system("You are {{ role }}"),
Message.request("Tell me about {{ topic }}"),
Message.response("The {{ topic }} is interesting"),
];
expect(extractVariablesFromMessages(messages)).toEqual(["role", "topic"]);
});
it("deduplicates across messages", () => {
const messages = [Message.system("{{ name }}"), Message.request("{{ name }}")];
expect(extractVariablesFromMessages(messages)).toEqual(["name"]);
});
it("returns empty array when no messages have variables", () => {
const messages = [Message.system("You are a helpful assistant"), Message.request("Hello")];
expect(extractVariablesFromMessages(messages)).toEqual([]);
});
it("returns empty array for empty messages array", () => {
expect(extractVariablesFromMessages([])).toEqual([]);
});
it("handles messages with empty content", () => {
const messages = [Message.system("")];
expect(extractVariablesFromMessages(messages)).toEqual([]);
});
it("handles error messages gracefully", () => {
const messages = [Message.error("Something went wrong")];
expect(extractVariablesFromMessages(messages)).toEqual([]);
});
});
// =============================================================================
// replaceVariablesInText
// =============================================================================
describe("replaceVariablesInText", () => {
it("replaces a single variable", () => {
expect(replaceVariablesInText("Hello {{ name }}", { name: "World" })).toBe("Hello World");
});
it("replaces multiple variables", () => {
const result = replaceVariablesInText("{{ greeting }}, {{ name }}!", {
greeting: "Hi",
name: "Alice",
});
expect(result).toBe("Hi, Alice!");
});
it("replaces all occurrences of the same variable", () => {
expect(replaceVariablesInText("{{ x }} and {{ x }}", { x: "yes" })).toBe("yes and yes");
});
it("preserves leading curly braces that are not part of variables", () => {
expect(replaceVariablesInText("{{{ x }} and {{ x }}", { x: "yes" })).toBe("{yes and yes");
});
it("preserves trailing curly braces that are not part of variables", () => {
expect(replaceVariablesInText("{{ x }} and {{ x }}}}", { x: "yes" })).toBe("yes and yes}}");
});
it("leaves variable untouched when not in map", () => {
expect(replaceVariablesInText("{{ unknown }}", {})).toBe("{{ unknown }}");
});
it("leaves variable untouched when value is empty string", () => {
expect(replaceVariablesInText("{{ name }}", { name: "" })).toBe("{{ name }}");
});
it("returns original text when no variables present", () => {
expect(replaceVariablesInText("Hello world", { name: "test" })).toBe("Hello world");
});
it("returns empty string for empty input", () => {
expect(replaceVariablesInText("", { name: "test" })).toBe("");
});
it("handles replacement value containing special regex characters", () => {
expect(replaceVariablesInText("{{ val }}", { val: "$100.00" })).toBe("$100.00");
});
it("handles replacement value containing curly braces", () => {
expect(replaceVariablesInText("{{ val }}", { val: "{{ nested }}" })).toBe("{{ nested }}");
});
it("handles variable with no spaces in braces", () => {
expect(replaceVariablesInText("{{name}}", { name: "Bob" })).toBe("Bob");
});
it("handles variable with extra whitespace in braces", () => {
expect(replaceVariablesInText("{{ name }}", { name: "Bob" })).toBe("Bob");
});
it("replaces only known variables and leaves others", () => {
const result = replaceVariablesInText("{{ known }} and {{ unknown }}", { known: "yes" });
expect(result).toBe("yes and {{ unknown }}");
});
it("handles multiline text replacement", () => {
const text = `Hello {{ name }},
Your order {{ order_id }} is ready.`;
const result = replaceVariablesInText(text, { name: "Alice", order_id: "12345" });
expect(result).toBe(`Hello Alice,
Your order 12345 is ready.`);
});
it("handles dot-notation variables", () => {
expect(replaceVariablesInText("{{ user.name }}", { "user.name": "Alice" })).toBe("Alice");
});
// Regression: consecutive calls should work (lastIndex reset)
it("works correctly when called multiple times in succession", () => {
expect(replaceVariablesInText("{{ a }}", { a: "1" })).toBe("1");
expect(replaceVariablesInText("{{ b }}", { b: "2" })).toBe("2");
});
});
// =============================================================================
// replaceVariablesInMessages
// =============================================================================
describe("replaceVariablesInMessages", () => {
it("replaces variables in message content", () => {
const messages = [Message.system("You are {{ role }}")];
const result = replaceVariablesInMessages(messages, { role: "a pirate" });
expect(result[0].content).toBe("You are a pirate");
});
it("does not mutate original messages", () => {
const messages = [Message.system("You are {{ role }}")];
replaceVariablesInMessages(messages, { role: "a pirate" });
expect(messages[0].content).toBe("You are {{ role }}");
});
it("returns original messages array when all variable values are empty", () => {
const messages = [Message.system("You are {{ role }}")];
const result = replaceVariablesInMessages(messages, { role: "" });
expect(result).toBe(messages); // same reference — fast path
});
it("returns original messages array when variables map is empty", () => {
const messages = [Message.system("You are {{ role }}")];
const result = replaceVariablesInMessages(messages, {});
expect(result).toBe(messages);
});
it("replaces variables across multiple messages", () => {
const messages = [Message.system("You are {{ role }}"), Message.request("Tell me about {{ topic }}")];
const result = replaceVariablesInMessages(messages, { role: "a teacher", topic: "math" });
expect(result[0].content).toBe("You are a teacher");
expect(result[1].content).toBe("Tell me about math");
});
it("preserves messages without variables unchanged", () => {
const messages = [Message.system("Hello"), Message.request("{{ name }}")];
const result = replaceVariablesInMessages(messages, { name: "Alice" });
expect(result[0].content).toBe("Hello");
expect(result[1].content).toBe("Alice");
});
it("preserves message count", () => {
const messages = [Message.system("{{ a }}"), Message.request("{{ b }}"), Message.response("{{ c }}")];
const result = replaceVariablesInMessages(messages, { a: "1", b: "2", c: "3" });
expect(result).toHaveLength(3);
});
it("handles empty messages array", () => {
const result = replaceVariablesInMessages([], { name: "Alice" });
expect(result).toEqual([]);
});
it("handles messages with empty content", () => {
const messages = [Message.system("")];
const result = replaceVariablesInMessages(messages, { name: "Alice" });
expect(result[0].content).toBe("");
});
});
// =============================================================================
// mergeVariables
// =============================================================================
describe("mergeVariables", () => {
it("creates entries for new variable names with empty values", () => {
expect(mergeVariables({}, ["name", "topic"])).toEqual({
name: "",
topic: "",
});
});
it("preserves existing values for variables that still exist", () => {
const current = { name: "Alice", topic: "math" };
const result = mergeVariables(current, ["name", "topic"]);
expect(result).toEqual({ name: "Alice", topic: "math" });
});
it("drops variables no longer in the new names list", () => {
const current = { name: "Alice", old_var: "value" };
const result = mergeVariables(current, ["name"]);
expect(result).toEqual({ name: "Alice" });
expect(result).not.toHaveProperty("old_var");
});
it("adds new variables while preserving existing ones", () => {
const current = { name: "Alice" };
const result = mergeVariables(current, ["name", "topic"]);
expect(result).toEqual({ name: "Alice", topic: "" });
});
it("handles empty current variables", () => {
expect(mergeVariables({}, ["a", "b"])).toEqual({ a: "", b: "" });
});
it("handles empty new names (returns empty map)", () => {
const current = { name: "Alice", topic: "math" };
expect(mergeVariables(current, [])).toEqual({});
});
it("handles both empty", () => {
expect(mergeVariables({}, [])).toEqual({});
});
it("does not mutate the original variables map", () => {
const current = { name: "Alice" };
mergeVariables(current, ["name", "topic"]);
expect(current).toEqual({ name: "Alice" });
});
it("preserves empty string values for existing variables", () => {
const current = { name: "" };
const result = mergeVariables(current, ["name"]);
expect(result).toEqual({ name: "" });
});
});