using System.Text.Json; using ClawdDotNet.Core.Memory; using ClawdDotNet.Core.Storage; using ClawdDotNet.Core.Tools; using ClawdDotNet.Tools.Memory; using ClawdDotNet.Tools.Tests.FileRW; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; namespace ClawdDotNet.Tools.Tests.Memory; /// /// #607: The news agent remembered the whole, growing link list without a key at every run. /// As soon as the newest entry alone was above 8,000 characters, recall delivered not a /// single content any more - the agent would have considered every news item new again. /// public sealed class MemoryToolTests : IDisposable { private readonly string _dir = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N")); private readonly SqliteMemoryRepository _repository; private readonly MemoryTool _tool = new(); public MemoryToolTests() { Directory.CreateDirectory(_dir); _repository = new SqliteMemoryRepository(new SqliteStorage(Path.Combine(_dir, "state.db"))); } public void Dispose() { ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_dir); try { Directory.Delete(_dir, recursive: true); } catch { /* Cleaning up is secondary */ } } private Task Call(object input) => _tool.ExecuteAsync( JsonSerializer.SerializeToElement(input), new AgentToolContext("nachrichten", "inst", new Dictionary(), new NullStateStore(), NullLogger.Instance, CancellationToken.None, Memory: _repository), CancellationToken.None); private static string LinkList(int count) => string.Join("\n", Enumerable.Range(1, count).Select(i => $"https://www.tagesschau.de/inland/meldung-{i:D4}.html")); [Fact] public async Task A_single_oversized_entry_is_shortened_instead_of_dropped() { var list = LinkList(250); // ~11,500 characters (await Call(new { action = "remember", subject = "gemeldet", content = list })).Success.ShouldBeTrue(); var fetch = await Call(new { action = "recall", query = "gemeldet" }); fetch.Success.ShouldBeTrue(); fetch.Content.ShouldContain("meldung-0001.html", customMessage: "der Anfang bleibt sichtbar"); fetch.Content.ShouldContain("meldung-0250.html", customMessage: "das Ende - die neuesten Links - bleibt sichtbar"); fetch.Content.ShouldContain("truncated"); fetch.Content.ShouldNotContain("more results omitted"); fetch.Content.Length.ShouldBeLessThanOrEqualTo(8_000); } [Fact] public async Task If_the_newest_of_several_entries_is_too_large_it_stays_partly_visible() { await Call(new { action = "remember", subject = "gemeldet", content = LinkList(100) }); await Task.Delay(20); await Call(new { action = "remember", subject = "gemeldet", content = LinkList(220) }); var fetch = await Call(new { action = "recall", query = "gemeldet" }); fetch.Content.ShouldContain("meldung-0220.html"); fetch.Content.ShouldContain("1 more results omitted"); fetch.Content.Length.ShouldBeLessThanOrEqualTo(8_000); } [Fact] public async Task What_fits_stays_unshortened() { await Call(new { action = "remember", subject = "gemeldet", content = LinkList(20) }); var fetch = await Call(new { action = "recall", query = "gemeldet" }); fetch.Content.ShouldNotContain("truncated"); fetch.Content.ShouldContain("meldung-0010.html"); } [Fact] public async Task Remembering_again_without_key_under_the_same_subject_points_to_key() { var firstOne = await Call(new { action = "remember", subject = "gemeldet", content = LinkList(3) }); var secondOne = await Call(new { action = "remember", subject = "gemeldet", content = LinkList(5) }); firstOne.Content.ShouldNotContain("Note:"); secondOne.Content.ShouldContain("2 entries without a key"); secondOne.Content.ShouldContain("key"); } [Fact] public async Task With_key_it_is_updated_and_not_warned() { await Call(new { action = "remember", subject = "gemeldet", key = "gemeldete-links", content = LinkList(3) }); var secondOne = await Call(new { action = "remember", subject = "gemeldet", key = "gemeldete-links", content = LinkList(5) }); secondOne.Content.ShouldNotContain("Note:"); (await _repository.CountAsync(MemoryScope.Agent, "nachrichten", CancellationToken.None)).ShouldBe(1); } [Fact] public async Task An_entry_near_the_recall_limit_is_flagged_when_remembering() { var result = await Call(new { action = "remember", subject = "gemeldet", key = "links", content = LinkList(120) }); result.Content.ShouldContain("characters long"); } }