Files
Wiki/test/Wiki.Application.Tests/EncyclopediaStateTests.cs
T
claude-botandClaude Opus 5.5 179bc3bcbc feat(encyclopedia): consolidate nearby articles, plan hierarchy, room articles
Article vectors (titleDe | titleEn | scope, embedded locally, cosine in
memory) feed the assign candidates alongside the note-neighbour articles
and form the groups of a new consolidate step after assign: one bounded
call per group of up to 8 nearby articles, at most 60 per run, returning
merges (fold mechanics, redirects incl. public ones) and parent links
(no cycles anywhere in the plan). Groups are remembered by fingerprint
in _state.json.

PlannedArticle gains a parent; split children get the overview. The write
prompt marks main and sub-articles among its link targets (children
capped at 20); the article page shows the main article and the visible
sub-articles from the plan. Parent changes do not make articles stale.

The assign prompt also sends room notes to the room article; the write
prompt writes room articles from the room's angle.

The plan file gets version 3: the v2 plan reads as no plan, its state is
dropped, and the next run bootstraps. Prompts.Version 8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 14:36:59 +01:00

134 lines
5.8 KiB
C#

using Wiki.Application.Encyclopedia;
using Wiki.Core.Model;
namespace Wiki.Application.Tests;
public class EncyclopediaStateTests
{
private static readonly DateTimeOffset At = new(2026, 10, 24, 3, 0, 0, TimeSpan.Zero);
[Fact]
public void EncyclopediaStateJson_RoundTrip()
{
var state = new EncyclopediaState(
new Dictionary<string, SubjectAttempt>
{
["heating"] = new("abc", AttemptOutcome.Written, At),
["sauna"] = new("def", AttemptOutcome.Rejected, At),
},
new Dictionary<string, DateTimeOffset>(),
new Dictionary<string, string>(),
new Dictionary<string, DateTimeOffset>(),
new Dictionary<string, DateTimeOffset>());
var read = EncyclopediaStateJson.Read(EncyclopediaStateJson.Write(state));
Assert.Equal(state.Subjects, read.Subjects);
}
[Fact]
public void RoundTrip_FailedBatchesAndKept()
{
var state = new EncyclopediaState(
new Dictionary<string, SubjectAttempt>(),
new Dictionary<string, DateTimeOffset> { ["fp1"] = At },
new Dictionary<string, string> { ["heating"] = "fp2" },
new Dictionary<string, DateTimeOffset> { ["g1"] = At },
new Dictionary<string, DateTimeOffset> { ["g2"] = At });
var read = EncyclopediaStateJson.Read(EncyclopediaStateJson.Write(state));
Assert.Equal(state.FailedBatches, read.FailedBatches);
Assert.Equal(state.KeptArticles, read.KeptArticles);
Assert.Equal(state.ConsolidatedGroups, read.ConsolidatedGroups);
Assert.Equal(state.FailedGroups, read.FailedGroups);
}
[Fact]
public void Read_WithoutGroups_ReadsEmpty()
{
var read = EncyclopediaStateJson.Read("""{"subjects":{},"failedBatches":{},"keptArticles":{}}""");
Assert.Empty(read.ConsolidatedGroups);
Assert.Empty(read.FailedGroups);
}
[Fact]
public void GroupFingerprint_MembersAndWhatTheModelJudges_NotSources()
{
var a = new PlannedArticle("a", "A", "A", ArticleCategory.System, "About a.", ["n1"]);
var b = new PlannedArticle("b", "B", "B", ArticleCategory.Device, "About b.", ["n2"], Parent: "a");
var baseline = EncyclopediaState.GroupFingerprint([a, b]);
Assert.Matches("^[0-9a-f]{64}$", baseline);
Assert.Equal(baseline, EncyclopediaState.GroupFingerprint([b, a]));
Assert.Equal(baseline, EncyclopediaState.GroupFingerprint([a, b with { Sources = ["n2", "n3"] }]));
Assert.NotEqual(baseline, EncyclopediaState.GroupFingerprint([a]));
Assert.NotEqual(baseline, EncyclopediaState.GroupFingerprint([a, b with { Parent = null }]));
Assert.NotEqual(baseline, EncyclopediaState.GroupFingerprint([a, b with { Scope = "Other." }]));
Assert.NotEqual(baseline, EncyclopediaState.GroupFingerprint([a, b with { TitleEn = "Bee" }]));
Assert.NotEqual(baseline, EncyclopediaState.GroupFingerprint([a, b with { Category = ArticleCategory.Room }]));
}
[Fact]
public void Read_OldJsonWithFailedPlan_IgnoresIt()
{
var read = EncyclopediaStateJson.Read("""{"subjects":{},"failedPlan":{"noteSetFingerprint":"x","at":"2026-10-24T03:00:00+00:00"}}""");
Assert.Empty(read.Subjects);
Assert.Empty(read.FailedBatches);
Assert.Empty(read.KeptArticles);
Assert.Empty(read.ConsolidatedGroups);
Assert.DoesNotContain("failedPlan", EncyclopediaStateJson.Write(read));
}
[Fact]
public void BatchFingerprint_ChangesWhenANoteChanges()
{
var ab = EncyclopediaState.BatchFingerprint([TestNotes.Note("a", hash: "1"), TestNotes.Note("b", hash: "2")]);
var ba = EncyclopediaState.BatchFingerprint([TestNotes.Note("b", hash: "2"), TestNotes.Note("a", hash: "1")]);
var changed = EncyclopediaState.BatchFingerprint([TestNotes.Note("a", hash: "1"), TestNotes.Note("b", hash: "3")]);
var fewer = EncyclopediaState.BatchFingerprint([TestNotes.Note("a", hash: "1")]);
Assert.Equal(ab, ba);
Assert.NotEqual(ab, changed);
Assert.NotEqual(ab, fewer);
Assert.Matches("^[0-9a-f]{64}$", ab);
}
[Fact]
public void EncyclopediaStateJson_Malformed_ReadsEmpty()
{
var read = EncyclopediaStateJson.Read("{{{ nope");
Assert.Empty(read.Subjects);
}
[Fact]
public void EncyclopediaStateJson_NullEntry_Dropped()
{
var read = EncyclopediaStateJson.Read("""{"subjects":{"a":null,"b":{"fingerprint":"f","outcome":"written","at":"2026-10-24T03:00:00+00:00"}},"failedPlan":null}""");
Assert.Equal(["b"], read.Subjects.Keys);
}
[Fact]
public void Empty_DoesNotShareMutableState()
{
((Dictionary<string, SubjectAttempt>)EncyclopediaState.Empty.Subjects)["x"] = new("f", AttemptOutcome.Written, At);
Assert.Empty(EncyclopediaState.Empty.Subjects);
}
[Fact]
public void SubjectFingerprint_ChangesWithSourceHashOrTitle()
{
var planned = new PlannedArticle("heating", "Heizung", "Heating", ArticleCategory.System, "", ["a", "b"]);
var notes = new[] { TestNotes.Note("a", hash: "1"), TestNotes.Note("b", hash: "2") };
var baseline = EncyclopediaState.SubjectFingerprint(planned, notes);
Assert.Equal(baseline, EncyclopediaState.SubjectFingerprint(planned, notes));
Assert.Matches("^[0-9a-f]{64}$", baseline);
var changedHash = new[] { notes[0], TestNotes.Note("b", hash: "3") };
Assert.NotEqual(baseline, EncyclopediaState.SubjectFingerprint(planned, changedHash));
Assert.NotEqual(baseline, EncyclopediaState.SubjectFingerprint(planned with { TitleDe = "Heizungsanlage" }, notes));
Assert.NotEqual(baseline, EncyclopediaState.SubjectFingerprint(planned with { Scope = "Only boilers" }, notes));
}
}