feature/pi-refactor
1/**
2 * Tests for XMPP Agent Wrapper
3 *
4 * RED phase: Write these tests first, verify they fail
5 * GREEN phase: Implement minimal code to pass
6 * REFACTOR phase: Improve structure while keeping tests green
7 */
8
9import { describe, it, expect, beforeEach } from "vitest";
10import { XmppAgent } from "./agent-wrapper.js";
11import { getModel } from "@mariozechner/pi-ai";
12
13describe("XmppAgent", () => {
14 let agent: XmppAgent;
15 const testJid = "test@xmpp.sbr.pm";
16
17 beforeEach(async () => {
18 // Use Gemini for tests (we have GEMINI_API_KEY in environment)
19 const model = getModel("google", "gemini-2.0-flash");
20 agent = new XmppAgent(testJid, model);
21 });
22
23 describe("creation", () => {
24 it("should create agent with JID and model", async () => {
25 expect(agent).toBeDefined();
26 expect(agent.jid).toBe(testJid);
27 });
28
29 it("should initialize with empty message history", async () => {
30 const messages = agent.getMessages();
31 expect(messages).toHaveLength(0);
32 });
33 });
34
35 describe("message processing", () => {
36 it("should process simple message and return response", async () => {
37 const response = await agent.processMessage("Hello");
38
39 expect(response).toBeDefined();
40 expect(typeof response).toBe("string");
41 expect(response.length).toBeGreaterThan(0);
42 });
43
44 it("should maintain conversation context", async () => {
45 await agent.processMessage("My name is Alice");
46 const response = await agent.processMessage("What is my name?");
47
48 expect(response.toLowerCase()).toContain("alice");
49 });
50 });
51
52 describe("model prefix parsing", () => {
53 it("should switch model when prefix is used", async () => {
54 const response = await agent.processMessage("g: Hello from Gemini");
55
56 expect(response).toBeDefined();
57 // Message should have prefix stripped
58 const messages = agent.getMessages();
59 const lastUserMsg = messages.filter(m => m.role === "user").pop();
60 expect(lastUserMsg?.content).not.toContain("g:");
61 });
62
63 it("should use default model when no prefix", async () => {
64 const defaultModel = agent.getCurrentModel();
65 await agent.processMessage("Hello without prefix");
66
67 expect(agent.getCurrentModel()).toEqual(defaultModel);
68 });
69
70 it("should handle invalid prefix gracefully", async () => {
71 const response = await agent.processMessage("invalid-prefix: Test");
72
73 // Should process as regular message, not fail
74 expect(response).toBeDefined();
75 });
76 });
77
78 describe("slash commands", () => {
79 it("should return pong for /ping", async () => {
80 const response = await agent.processMessage("/ping");
81
82 expect(response).toBe("pong!");
83 });
84
85 it("should return help text for /help", async () => {
86 const response = await agent.processMessage("/help");
87
88 expect(response).toContain("Available commands:");
89 expect(response).toContain("/ping");
90 expect(response).toContain("/help");
91 });
92
93 it("should clear conversation for /clear", async () => {
94 await agent.processMessage("Remember this");
95 await agent.processMessage("/clear");
96
97 const messages = agent.getMessages();
98 expect(messages).toHaveLength(0);
99 });
100
101 it("should list models for /models", async () => {
102 const response = await agent.processMessage("/models");
103
104 expect(response).toContain("Available models:");
105 expect(response).toContain("opus:");
106 expect(response).toContain("g:");
107 });
108
109 it("should show stats for /stats", async () => {
110 await agent.processMessage("Test message 1");
111 await agent.processMessage("Test message 2");
112
113 const response = await agent.processMessage("/stats");
114
115 expect(response).toContain("messages");
116 expect(response).toMatch(/\d+/); // Contains numbers
117 });
118
119 it("should not call LLM for slash commands", async () => {
120 const messageCountBefore = agent.getMessages().length;
121
122 await agent.processMessage("/ping");
123
124 const messageCountAfter = agent.getMessages().length;
125 // /ping should not add messages to conversation
126 expect(messageCountAfter).toBe(messageCountBefore);
127 });
128 });
129
130 describe("session state", () => {
131 it("should track message count", async () => {
132 expect(agent.getMessageCount()).toBe(0);
133
134 await agent.processMessage("First");
135 expect(agent.getMessageCount()).toBeGreaterThan(0);
136
137 await agent.processMessage("Second");
138 expect(agent.getMessageCount()).toBeGreaterThan(2);
139 });
140
141 it("should provide current model info", async () => {
142 const model = agent.getCurrentModel();
143
144 expect(model).toBeDefined();
145 expect(model.provider).toBeDefined();
146 expect(model.id).toBeDefined();
147 });
148 });
149});