#include <limits>
#include <agent_smith/memory.hpp>
#include <agent_smith/sync_wait.hpp>
#include <gtest/gtest.h>
namespace agent_smith
{
namespace
{
std::vector<Message> snapshot(Memory& memory)
{
const SystemMessage system{"system"};
auto result = syncWait(memory.snapshot(system));
EXPECT_TRUE(result);
return result ? std::move(*result) : std::vector<Message>{};
}
AssistantMessage calls()
{
return {.content = "calling",
.tool_calls = {{"a", "weather", {{"city", "Tokyo"}}},
{"b", "weather", {{"city", "Paris"}}}}};
}
TEST(FullMemoryTest, PreservesHistoryAndCopiesSnapshots)
{
FullMemory memory;
EXPECT_EQ(snapshot(memory).size(), 1U);
memory.append(UserMessage{"hi"});
memory.append(calls());
memory.append(ToolMessage{"a", "weather", "sunny"});
memory.append(ToolMessage{"b", "weather", "rainy"});
memory.append(AssistantMessage{.content = "done"});
ASSERT_EQ(memory.size(), 5U);
auto first = snapshot(memory);
ASSERT_EQ(first.size(), 6U);
EXPECT_EQ(std::get<SystemMessage>(first[0]).content, "system");
EXPECT_EQ(std::get<AssistantMessage>(first[2]).tool_calls.size(), 2U);
std::get<UserMessage>(first[1]).content = "changed";
EXPECT_EQ(std::get<UserMessage>(snapshot(memory)[1]).content, "hi");
memory.clear();
EXPECT_EQ(memory.size(), 0U);
EXPECT_EQ(first.size(), 6U);
}
TEST(RollingWindowMemoryTest, RetainsNewestAndKeepsSystemOutsideBudget)
{
RollingWindowMemory memory(2);
memory.append(UserMessage{"old"});
memory.append(AssistantMessage{.content = "answer"});
memory.append(UserMessage{"new"});
auto result = snapshot(memory);
ASSERT_EQ(result.size(), 3U);
EXPECT_EQ(std::get<AssistantMessage>(result[1]).content, "answer");
EXPECT_EQ(std::get<UserMessage>(result[2]).content, "new");
memory.clear();
EXPECT_EQ(memory.size(), 0U);
memory.append(UserMessage{"again"});
EXPECT_EQ(memory.size(), 1U);
}
TEST(RollingWindowMemoryTest, AppliesCharacterAndTokenBudgets)
{
const Message message = UserMessage{"hello"};
const auto bytes = toOpenAIJson(message).dump().size();
RollingWindowMemory exact({.max_chars = bytes});
exact.append(message);
EXPECT_EQ(exact.size(), 1U);
exact.append(message);
EXPECT_EQ(exact.size(), 1U);
RollingWindowMemory too_small({.max_chars = bytes - 1});
too_small.append(message);
EXPECT_EQ(too_small.size(), 0U);
RollingWindowMemory tokens({.max_tokens = (bytes + 3) / 4});
tokens.append(message);
EXPECT_EQ(tokens.size(), 1U);
tokens.append(message);
EXPECT_EQ(tokens.size(), 1U);
RollingWindowMemory both({.max_messages = 0, .max_tokens = 100});
both.append(message);
EXPECT_EQ(snapshot(both).size(), 1U);
RollingWindowMemory huge({.max_tokens =
std::numeric_limits<std::size_t>::max()});
huge.append(message);
EXPECT_EQ(huge.size(), 1U);
}
TEST(RollingWindowMemoryTest, CountsToolArgumentsAndDropsEntireGroup)
{
RollingWindowMemory memory(3);
memory.append(calls());
memory.append(ToolMessage{"a", "weather", "sunny"});
memory.append(ToolMessage{"b", "weather", "rainy"});
EXPECT_EQ(memory.size(), 3U);
memory.append(UserMessage{"next"});
ASSERT_EQ(memory.size(), 1U);
EXPECT_EQ(std::get<UserMessage>(snapshot(memory)[1]).content, "next");
RollingWindowMemory small({.max_chars = 100});
auto large = calls();
large.tool_calls[0].arguments = {{"data", std::string(1000, 'x')}};
small.append(large);
small.append(ToolMessage{"a", "weather", "result"});
EXPECT_EQ(small.size(), 0U);
}
TEST(MemoryTest, RemovesOrphansDuplicatesAndCallsFromPreviousAssistant)
{
std::vector<Message> messages{
ToolMessage{"a", "weather", "orphan"}, calls(),
ToolMessage{"b", "weather", "second first"},
ToolMessage{"a", "weather", "first second"},
ToolMessage{"a", "weather", "duplicate"},
AssistantMessage{.content = "done"},
ToolMessage{"b", "weather", "stale"}};
Memory::stripOrphans(messages);
ASSERT_EQ(messages.size(), 4U);
EXPECT_EQ(std::get<ToolMessage>(messages[1]).tool_call_id, "b");
EXPECT_EQ(std::get<ToolMessage>(messages[2]).tool_call_id, "a");
Memory::stripOrphans(messages);
EXPECT_EQ(messages.size(), 4U);
}
TEST(RollingWindowMemoryTest, CombinedLimitsAndOversizeMessages)
{
const Message message = UserMessage{"hello"};
const auto bytes = toOpenAIJson(message).dump().size();
RollingWindowMemory memory({.max_messages = 3, .max_chars = bytes * 2,
.max_tokens = 1000});
for(int index = 0; index < 4; ++index)
{
memory.append(message);
}
EXPECT_EQ(memory.size(), 2U);
memory.append(UserMessage{std::string(1000, 'x')});
EXPECT_EQ(memory.size(), 0U);
memory.append(message);
EXPECT_EQ(memory.size(), 1U);
RollingWindowMemory zero({.max_tokens = 0});
zero.append(UserMessage{""});
EXPECT_EQ(zero.size(), 0U);
}
TEST(MemoryTest, ChangingSystemPromptDoesNotAccumulateHistory)
{
FullMemory full;
RollingWindowMemory rolling(4);
for(Memory* memory : std::vector<Memory*>{&full, &rolling})
{
memory->append(SystemMessage{"stored instruction"});
memory->append(UserMessage{"你好"});
const SystemMessage first{"first"};
const SystemMessage second{"second"};
auto original = syncWait(memory->snapshot(first));
auto updated = syncWait(memory->snapshot(second));
ASSERT_TRUE(original);
ASSERT_TRUE(updated);
ASSERT_EQ(updated->size(), 3U);
EXPECT_EQ(std::get<SystemMessage>((*original)[0]).content, "first");
EXPECT_EQ(std::get<SystemMessage>((*updated)[0]).content, "second");
EXPECT_EQ(std::get<SystemMessage>((*updated)[1]).content,
"stored instruction");
EXPECT_EQ(memory->size(), 2U);
}
}
} // namespace
} // namespace agent_smith