Mit Roslyn umbenannt (Verweise folgen, Zeichenketten bleiben unberuehrt). Nicht umbenannt, weil gespeichert oder ueber die API gebunden (Stufe 4e): Properties, Record-Parameter, Enum-Member sowie die Parameter der Endpunkt-Handler (Routen-/Query-Bindung ueber den Namen). Die JSON-Felder anonymer Objekte bleiben ausdruecklich benannt (JsonFilter: pfad). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
4.7 KiB
C#
115 lines
4.7 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// #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.
|
|
/// </summary>
|
|
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<ToolResult> Call(object input) => _tool.ExecuteAsync(
|
|
JsonSerializer.SerializeToElement(input),
|
|
new AgentToolContext("nachrichten", "inst", new Dictionary<string, object?>(), 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");
|
|
}
|
|
}
|