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});