Commit b0eb9b4

mo khan <mo@mokhan.ca>
2025-11-05 23:59:34
test: generate specs
1 parent cdee182
lib/elelem/conversation.rb
@@ -56,7 +56,7 @@ module Elelem
         "#{base}\n\nUse commands to deeply understand the system."
       when [:execute, :write]
         "#{base}\n\nCreate and execute freely. Have fun. Be kind."
-      when [:read, :execute, :write]
+      when [:execute, :read, :write]
         "#{base}\n\nYou have all tools. Use them wisely."
       else
         base
spec/elelem/agent_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+RSpec.describe Elelem::Agent do
+  let(:mock_client) { double("client") }
+  let(:agent) { described_class.new(mock_client) }
+
+  describe "#initialize" do
+    it "creates a new conversation" do
+      expect(agent.conversation).to be_a(Elelem::Conversation)
+    end
+
+    it "stores the client" do
+      expect(agent.client).to eq(mock_client)
+    end
+
+    it "initializes tools for all modes" do
+      expect(agent.tools[:read]).to be_an(Array)
+      expect(agent.tools[:write]).to be_an(Array)
+      expect(agent.tools[:execute]).to be_an(Array)
+    end
+  end
+
+  describe "#tools_for" do
+    it "returns read tools for read mode" do
+      mode = Set[:read]
+      tools = agent.send(:tools_for, mode)
+
+      tool_names = tools.map { |t| t.dig(:function, :name) }
+      expect(tool_names).to include("grep", "list", "read")
+      expect(tool_names).not_to include("write", "patch", "execute")
+    end
+
+    it "returns write tools for write mode" do
+      mode = Set[:write]
+      tools = agent.send(:tools_for, mode)
+
+      tool_names = tools.map { |t| t.dig(:function, :name) }
+      expect(tool_names).to include("patch", "write")
+      expect(tool_names).not_to include("grep", "execute")
+    end
+
+    it "returns execute tools for execute mode" do
+      mode = Set[:execute]
+      tools = agent.send(:tools_for, mode)
+
+      tool_names = tools.map { |t| t.dig(:function, :name) }
+      expect(tool_names).to include("execute")
+      expect(tool_names).not_to include("grep", "write")
+    end
+
+    it "returns all tools for auto mode" do
+      mode = Set[:read, :write, :execute]
+      tools = agent.send(:tools_for, mode)
+
+      tool_names = tools.map { |t| t.dig(:function, :name) }
+      expect(tool_names).to include("grep", "list", "read", "patch", "write", "execute")
+    end
+
+    it "returns combined tools for build mode" do
+      mode = Set[:read, :write]
+      tools = agent.send(:tools_for, mode)
+
+      tool_names = tools.map { |t| t.dig(:function, :name) }
+      expect(tool_names).to include("grep", "read", "write", "patch")
+      expect(tool_names).not_to include("execute")
+    end
+  end
+
+  describe "integration with conversation" do
+    it "conversation uses mode-aware prompts" do
+      conversation = agent.conversation
+      conversation.add(role: :user, content: "test message")
+
+      read_history = conversation.history_for([:read])
+      write_history = conversation.history_for([:write])
+
+      expect(read_history[0][:content]).to include("Read and analyze")
+      expect(write_history[0][:content]).to include("Write clean, thoughtful code")
+      expect(read_history[0][:content]).not_to eq(write_history[0][:content])
+    end
+  end
+end
spec/elelem/conversation_spec.rb
@@ -0,0 +1,188 @@
+# frozen_string_literal: true
+
+RSpec.describe Elelem::Conversation do
+  let(:conversation) { described_class.new }
+
+  describe "#history_for" do
+    context "with empty conversation" do
+      it "returns history with mode-specific system prompt for read mode" do
+        history = conversation.history_for([:read])
+
+        expect(history.length).to eq(1)
+        expect(history[0][:role]).to eq("system")
+        expect(history[0][:content]).to include("Read and analyze")
+      end
+
+      it "returns history with mode-specific system prompt for write mode" do
+        history = conversation.history_for([:write])
+
+        expect(history[0][:content]).to include("Write clean, thoughtful code")
+      end
+
+      it "returns history with mode-specific system prompt for execute mode" do
+        history = conversation.history_for([:execute])
+
+        expect(history[0][:content]).to include("Use shell commands creatively")
+      end
+
+      it "returns history with mode-specific system prompt for read+write mode" do
+        history = conversation.history_for([:read, :write])
+
+        expect(history[0][:content]).to include("First understand, then build solutions")
+      end
+
+      it "returns history with mode-specific system prompt for read+execute mode" do
+        history = conversation.history_for([:read, :execute])
+
+        expect(history[0][:content]).to include("Use commands to deeply understand")
+      end
+
+      it "returns history with mode-specific system prompt for write+execute mode" do
+        history = conversation.history_for([:write, :execute])
+
+        expect(history[0][:content]).to include("Create and execute freely")
+      end
+
+      it "returns history with mode-specific system prompt for all tools mode" do
+        history = conversation.history_for([:read, :write, :execute])
+
+        expect(history[0][:content]).to include("You have all tools")
+      end
+
+      it "returns base system prompt for unknown mode" do
+        history = conversation.history_for([:unknown])
+
+        expect(history[0][:content]).not_to include("Read and analyze")
+        expect(history[0][:content]).not_to include("Write clean")
+      end
+
+      it "returns base system prompt for empty mode" do
+        history = conversation.history_for([])
+
+        expect(history[0][:role]).to eq("system")
+        expect(history[0][:content]).to be_a(String)
+      end
+    end
+
+    context "with mode order independence" do
+      it "returns same prompt for [:read, :write] and [:write, :read]" do
+        history1 = conversation.history_for([:read, :write])
+        history2 = conversation.history_for([:write, :read])
+
+        expect(history1[0][:content]).to eq(history2[0][:content])
+      end
+
+      it "returns same prompt for [:read, :execute] and [:execute, :read]" do
+        history1 = conversation.history_for([:read, :execute])
+        history2 = conversation.history_for([:execute, :read])
+
+        expect(history1[0][:content]).to eq(history2[0][:content])
+      end
+
+      it "returns same prompt for all permutations of [:read, :write, :execute]" do
+        history1 = conversation.history_for([:read, :write, :execute])
+        history2 = conversation.history_for([:execute, :read, :write])
+        history3 = conversation.history_for([:write, :execute, :read])
+
+        expect(history1[0][:content]).to eq(history2[0][:content])
+        expect(history2[0][:content]).to eq(history3[0][:content])
+      end
+    end
+
+    context "with populated conversation" do
+      before do
+        conversation.add(role: :user, content: "Hello")
+        conversation.add(role: :assistant, content: "Hi there")
+      end
+
+      it "preserves all conversation items" do
+        history = conversation.history_for([:read])
+
+        expect(history.length).to eq(3)
+        expect(history[1][:role]).to eq(:user)
+        expect(history[1][:content]).to eq("Hello")
+        expect(history[2][:role]).to eq(:assistant)
+        expect(history[2][:content]).to eq("Hi there")
+      end
+
+      it "updates system prompt without mutating original" do
+        original_items = conversation.instance_variable_get(:@items)
+        original_system_content = original_items[0][:content]
+
+        history = conversation.history_for([:read])
+
+        expect(history[0][:content]).not_to eq(original_system_content)
+        expect(original_items[0][:content]).to eq(original_system_content)
+      end
+
+      it "returns a copy, not the original array" do
+        history = conversation.history_for([:read])
+        original_items = conversation.instance_variable_get(:@items)
+
+        expect(history).not_to be(original_items)
+      end
+    end
+  end
+
+  describe "#add" do
+    it "adds user message to conversation" do
+      conversation.add(role: :user, content: "test message")
+      history = conversation.history_for([])
+
+      expect(history.length).to eq(2)
+      expect(history[1][:content]).to eq("test message")
+    end
+
+    it "merges consecutive messages with same role" do
+      conversation.add(role: :user, content: "part 1")
+      conversation.add(role: :user, content: "part 2")
+      history = conversation.history_for([])
+
+      expect(history.length).to eq(2)
+      expect(history[1][:content]).to eq("part 1part 2")
+    end
+
+    it "ignores nil content" do
+      conversation.add(role: :user, content: nil)
+      history = conversation.history_for([])
+
+      expect(history.length).to eq(1)
+    end
+
+    it "ignores empty content" do
+      conversation.add(role: :user, content: "")
+      history = conversation.history_for([])
+
+      expect(history.length).to eq(1)
+    end
+
+    it "raises error for unknown role" do
+      expect {
+        conversation.add(role: :unknown, content: "test")
+      }.to raise_error(/unknown role/)
+    end
+  end
+
+  describe "#clear" do
+    it "resets conversation to default context" do
+      conversation.add(role: :user, content: "test")
+      conversation.clear
+      history = conversation.history_for([])
+
+      expect(history.length).to eq(1)
+      expect(history[0][:role]).to eq("system")
+    end
+  end
+
+  describe "#dump" do
+    it "returns JSON representation with mode-specific prompt" do
+      conversation.add(role: :user, content: "test")
+      json = conversation.dump([:read])
+
+      parsed = JSON.parse(json)
+      expect(parsed).to be_an(Array)
+      expect(parsed.length).to eq(2)
+      expect(parsed[0]["content"]).to include("Read and analyze")
+    end
+  end
+end
spec/spec_helper.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require "elelem"
+require_relative "../lib/elelem"
 
 RSpec.configure do |config|
   # Enable flags like --only-failures and --next-failure