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: Der Nachrichten-Agent merkte sich bei jedem Lauf die ganze, wachsende Linkliste ohne /// Schluessel. Sobald der neueste Eintrag allein ueber 8.000 Zeichen lag, lieferte recall keinen /// einzigen Inhalt mehr - der Agent haette jede Meldung erneut fuer neu gehalten. /// 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 { /* Aufraeumen ist Nebensache */ } } private Task Aufruf(object eingabe) => _tool.ExecuteAsync( JsonSerializer.SerializeToElement(eingabe), new AgentToolContext("nachrichten", "inst", new Dictionary(), new NullStateStore(), NullLogger.Instance, CancellationToken.None, Memory: _repository), CancellationToken.None); private static string Linkliste(int anzahl) => string.Join("\n", Enumerable.Range(1, anzahl).Select(i => $"https://www.tagesschau.de/inland/meldung-{i:D4}.html")); [Fact] public async Task Ein_einzelner_uebergrosser_Eintrag_wird_gekuerzt_statt_weggelassen() { var liste = Linkliste(250); // ~11.500 Zeichen (await Aufruf(new { action = "remember", subject = "gemeldet", content = liste })).Success.ShouldBeTrue(); var abruf = await Aufruf(new { action = "recall", query = "gemeldet" }); abruf.Success.ShouldBeTrue(); abruf.Content.ShouldContain("meldung-0001.html", customMessage: "der Anfang bleibt sichtbar"); abruf.Content.ShouldContain("meldung-0250.html", customMessage: "das Ende - die neuesten Links - bleibt sichtbar"); abruf.Content.ShouldContain("truncated"); abruf.Content.ShouldNotContain("more results omitted"); abruf.Content.Length.ShouldBeLessThanOrEqualTo(8_000); } [Fact] public async Task Ist_der_neueste_von_mehreren_Eintraegen_zu_gross_bleibt_er_teilweise_sichtbar() { await Aufruf(new { action = "remember", subject = "gemeldet", content = Linkliste(100) }); await Task.Delay(20); await Aufruf(new { action = "remember", subject = "gemeldet", content = Linkliste(220) }); var abruf = await Aufruf(new { action = "recall", query = "gemeldet" }); abruf.Content.ShouldContain("meldung-0220.html"); abruf.Content.ShouldContain("1 more results omitted"); abruf.Content.Length.ShouldBeLessThanOrEqualTo(8_000); } [Fact] public async Task Was_passt_bleibt_ungekuerzt() { await Aufruf(new { action = "remember", subject = "gemeldet", content = Linkliste(20) }); var abruf = await Aufruf(new { action = "recall", query = "gemeldet" }); abruf.Content.ShouldNotContain("truncated"); abruf.Content.ShouldContain("meldung-0010.html"); } [Fact] public async Task Erneutes_Merken_ohne_Schluessel_unter_demselben_Betreff_weist_auf_key_hin() { var erster = await Aufruf(new { action = "remember", subject = "gemeldet", content = Linkliste(3) }); var zweiter = await Aufruf(new { action = "remember", subject = "gemeldet", content = Linkliste(5) }); erster.Content.ShouldNotContain("Note:"); zweiter.Content.ShouldContain("2 entries without a key"); zweiter.Content.ShouldContain("key"); } [Fact] public async Task Mit_Schluessel_wird_fortgeschrieben_und_nicht_gewarnt() { await Aufruf(new { action = "remember", subject = "gemeldet", key = "gemeldete-links", content = Linkliste(3) }); var zweiter = await Aufruf(new { action = "remember", subject = "gemeldet", key = "gemeldete-links", content = Linkliste(5) }); zweiter.Content.ShouldNotContain("Note:"); (await _repository.CountAsync(MemoryScope.Agent, "nachrichten", CancellationToken.None)).ShouldBe(1); } [Fact] public async Task Ein_Eintrag_nahe_der_Abrufgrenze_wird_beim_Merken_angemahnt() { var ergebnis = await Aufruf(new { action = "remember", subject = "gemeldet", key = "links", content = Linkliste(120) }); ergebnis.Content.ShouldContain("characters long"); } }