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>
134 lines
5.8 KiB
C#
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));
|
|
}
|
|
}
|