first commit
This commit is contained in:
363
ui/lib/message/variables.test.ts
Normal file
363
ui/lib/message/variables.test.ts
Normal file
@@ -0,0 +1,363 @@
|
||||
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: "" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user