Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bb4b09b
Add sorting and filtering on token aware headwords
myieye Mar 16, 2026
9aceb95
Fix build errors
myieye Mar 17, 2026
d5c8b7f
Remove Headword from Entity class
myieye Mar 24, 2026
1f48cea
Revert Headword -> HeadwordText rename
myieye Mar 24, 2026
e9d7f09
Query morph-types async
myieye Mar 24, 2026
f367d9e
Remove deprecated type
myieye Mar 24, 2026
be8dbbd
Use more intuitive sort order (functionally the same)
myieye Mar 24, 2026
823531a
Tidy up tests and test comments
myieye Mar 24, 2026
756e68f
Seed canonical morph-types into CRDT projects
claude Mar 18, 2026
98448b8
Make SecondaryOrder explicit on all morph types, add reverse coverage…
claude Mar 19, 2026
694ff1a
Stop creating morph-types in tests. They're now prepopulated
myieye Mar 24, 2026
0ae757e
Add tasks for running and resetting verification tests (#2216)
myieye Mar 19, 2026
36cd084
Remove references to delete MorphTypeKind.Other
myieye Mar 24, 2026
53a0b95
Stop printing verify diff content. It's too much.
myieye Mar 24, 2026
f8f4cfe
Fix test
myieye Mar 24, 2026
3892351
Merge branch 'feat/sync-morph-types' into claude/fix-morph-type-synci…
rmunn Mar 30, 2026
da8250a
Seed morph types before API testing
rmunn Mar 31, 2026
c4c86bd
ResumableTests should expect real morph types now
rmunn Mar 31, 2026
ae99d97
Fix bad LLM-generated test
rmunn Apr 1, 2026
99915a8
Fix another wrong pre-cleanup filename
rmunn Apr 1, 2026
5e3dc1d
Add descriptions to canonical morph types
rmunn Apr 1, 2026
1e94608
Don't depend on entry order in snapshots
rmunn Apr 1, 2026
f93bdb7
Sort entries identically in FW and CRDT APIs
rmunn Apr 2, 2026
19c4e05
Mention correct SecondaryOrder in test comments
rmunn Apr 2, 2026
b780ecc
Address my own review comments
rmunn Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ public async Task SecondaryOrder_DefaultsToStem(string query, SortField sortFiel
{
var unknownMorphTypeEntryId = Guid.NewGuid();
Entry[] expected = [
new() { Id = unknownMorphTypeEntryId, LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.Unknown }, // SecondaryOrder defaults to Stem = 1
new() { Id = Guid.NewGuid(), LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.BoundStem }, // SecondaryOrder = 2
new() { Id = Guid.NewGuid(), LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.Suffix }, // SecondaryOrder = 6
new() { Id = unknownMorphTypeEntryId, LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.Unknown }, // SecondaryOrder defaults to Stem = 0
new() { Id = Guid.NewGuid(), LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.BoundStem }, // SecondaryOrder = 10
new() { Id = Guid.NewGuid(), LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.Suffix }, // SecondaryOrder = 70
];

var ids = expected.Select(e => e.Id).ToHashSet();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
using FluentAssertions.Extensibility;
using FwLiteProjectSync.Tests;

Expand All @@ -7,6 +8,12 @@ namespace FwLiteProjectSync.Tests;

public static class FluentAssertGlobalConfig
{
[ModuleInitializer]
internal static void InitVerify()
{
VerifierSettings.OmitContentFromException();
}

public static void Initialize()
{
MiniLcm.Tests.FluentAssertGlobalConfig.Initialize();
Expand Down
15 changes: 2 additions & 13 deletions backend/FwLite/FwLiteProjectSync.Tests/Import/ResumableTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using LcmCrdt.Objects;
using LcmCrdt.Tests;
using Microsoft.Extensions.Logging.Abstractions;
using MiniLcm;
Expand Down Expand Up @@ -38,18 +39,7 @@ public async Task ImportProject_IsResumable_AcrossRandomFailures()
}).ToList();
var expectedPartsOfSpeech = Enumerable.Range(1, 10)
.Select(i => new PartOfSpeech { Id = Guid.NewGuid(), Name = { ["en"] = $"pos{i}" } }).ToList();
var expectedMorphTypes = Enum.GetValues<MorphTypeKind>()
.Select(typ => new MorphType()
{
Id = Guid.NewGuid(),
Name = new() { ["en"] = $"Test Morph Type {(int)typ} {typ}" },
Abbreviation = new() { ["en"] = $"Tst MrphTyp{(int)typ}" },
Description = new() { { "en", new RichString($"test desc for {typ}") } },
Prefix = null,
Postfix = null,
Kind = typ,
SecondaryOrder = 0
}).ToList();
var expectedMorphTypes = CanonicalMorphTypes.All.Values;

var mockFrom = new Mock<IMiniLcmApi>();
IMiniLcmApi mockTo = new UnreliableApi(
Expand Down Expand Up @@ -132,7 +122,6 @@ public async Task ImportProject_IsResumable_AcrossRandomFailures()
createdEntries.Select(e => e.LexemeForm["en"]).Should().BeEquivalentTo(expectedEntries.Select(e => e.LexemeForm["en"]));
createdMorphTypes.Select(e => e.Name["en"]).Should().BeEquivalentTo(expectedMorphTypes.Select(e => e.Name["en"]));
createdMorphTypes.Select(e => e.Kind).Should().BeEquivalentTo(expectedMorphTypes.Select(e => e.Kind));

}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public async Task AssertSena3Snapshots(string sourceSnapshotName)
}

[Fact]
[Trait("Category", "Verified")]
public async Task LatestSena3SnapshotRoundTrips()
{
// arrange
Expand Down
30 changes: 30 additions & 0 deletions backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,35 @@ private async Task WorkaroundMissingWritingSystems()

}

[Fact]
[Trait("Category", "Integration")]
public async Task CanonicalMorphTypes_MatchFwDataMorphTypes()
{
var fwDataMorphTypes = await _fwDataApi.GetMorphTypes().ToArrayAsync();
fwDataMorphTypes.Should().NotBeEmpty("Sena 3 should have morph types");

// Verify every FwData morph type has a matching canonical entry
foreach (var fwMorphType in fwDataMorphTypes)
{
fwMorphType.Kind.Should().NotBe(MorphTypeKind.Unknown);

CanonicalMorphTypes.All.Should().ContainKey(fwMorphType.Kind,
$"canonical morph types should include {fwMorphType.Kind}");
var canonical = CanonicalMorphTypes.All[fwMorphType.Kind];
canonical.Id.Should().Be(fwMorphType.Id, $"GUID for {fwMorphType.Kind} should match FwData");
canonical.Prefix.Should().Be(fwMorphType.Prefix, $"Prefix for {fwMorphType.Kind} should match FwData");
canonical.Postfix.Should().Be(fwMorphType.Postfix, $"Postfix for {fwMorphType.Kind} should match FwData");
canonical.SecondaryOrder.Should().Be(fwMorphType.SecondaryOrder, $"SecondaryOrder for {fwMorphType.Kind} should match FwData");
}

// Verify every canonical morph type exists in FwData (no extras we shouldn't have)
var fwDataKinds = fwDataMorphTypes
.Select(m => m.Kind)
.ToHashSet();
CanonicalMorphTypes.All.Keys.Should().BeEquivalentTo(fwDataKinds,
"every canonical morph type should exist in the Sena 3 FwData project");
}

[Fact]
[Trait("Category", "Integration")]
public async Task DryRunImport_MakesNoChanges()
Expand Down Expand Up @@ -207,6 +236,7 @@ public async Task SecondSena3SyncDoesNothing()
/// </summary>
[Fact]
[Trait("Category", "Integration")]
[Trait("Category", "Verified")]
public async Task LiveSena3Sync()
{
// arrange - put "live" crdt db and fw-headless snapshot in place
Expand Down
28 changes: 28 additions & 0 deletions backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -678,4 +678,32 @@ public async Task CanCreateAComplexFormTypeAndSyncsIt()

_fixture.FwDataApi.GetComplexFormTypes().ToBlockingEnumerable().Should().ContainEquivalentOf(complexFormEntry);
}

[Fact]
[Trait("Category", "Integration")]
public async Task SyncWithLegacySnapshot_EmptyMorphTypes_DoesNotDuplicate()
{
var crdtApi = _fixture.CrdtApi;
var fwdataApi = _fixture.FwDataApi;

// First sync: import so both sides have data
await _syncService.Import(crdtApi, fwdataApi);
var snapshot = await _fixture.RegenerateAndGetSnapshot();

// Simulate a legacy snapshot by clearing MorphTypes
var legacySnapshot = snapshot with { MorphTypes = [] };

// The CRDT should already have morph types (from seeding in MigrateDb).
// Syncing with a legacy snapshot should patch the snapshot and not duplicate morph types.
var syncResult = await _syncService.Sync(crdtApi, fwdataApi, legacySnapshot);

// Verify no duplicates
var crdtMorphTypes = await crdtApi.GetMorphTypes().ToArrayAsync();
crdtMorphTypes.Should().OnlyHaveUniqueItems(mt => mt.Kind);
crdtMorphTypes.Should().NotBeEmpty();

// Verify no morph-type changes were needed (they were patched from CRDT)
syncResult.CrdtChanges.Should().Be(0);
syncResult.FwdataChanges.Should().Be(0);
}
}
12 changes: 12 additions & 0 deletions backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ private async Task<SyncResult> SyncOrImportInternal(IMiniLcmApi crdtApi, IMiniLc
{
// Repair any missing translation IDs before doing the full sync, so the sync doesn't have to deal with them
var syncedIdCount = await CrdtRepairs.SyncMissingTranslationIds(projectSnapshot.Entries, fwdata, crdt, dryRun);

// Patch legacy snapshots that were created before morph-type support.
// After seeding, the CRDT has morph-types but the snapshot still has [].
// Without this patch, the diff would see all morph-types as "new" and try to re-add them.
if (projectSnapshot.MorphTypes.Length == 0)
{
var currentCrdtMorphTypes = await crdt.GetMorphTypes().ToArrayAsync();
if (currentCrdtMorphTypes.Length > 0)
{
projectSnapshot = projectSnapshot with { MorphTypes = currentCrdtMorphTypes };
}
}
}

var syncResult = projectSnapshot is null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public void CanDeserializeLegacyRegressionData()
}

[Fact]
[Trait("Category", "Verified")]
public async Task RegressionDataUpToDate()
{
var legacyJsonArray = ReadJsonArrayFromFile(GetJsonFilePath("ChangeDeserializationRegressionData.legacy.verified.txt"));
Expand Down
4 changes: 2 additions & 2 deletions backend/FwLite/LcmCrdt.Tests/Data/FilteringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ public FilteringTests()
_entries =
[
new Entry { LexemeForm = { { "en", "123" } }, },
new Entry { LexemeForm = { { "en", "456" } }, }
new Entry { LexemeForm = { { "en", "456" } }, },
];
_morphTypes = new MorphType[] { new() { Id = Guid.NewGuid(), Kind = MorphTypeKind.Stem, Name = { ["en"] = "Stem" }, SecondaryOrder = 1 } }.AsQueryable();
_morphTypes = CanonicalMorphTypes.All.Values.ToArray().AsQueryable();
}

[Theory]
Expand Down
3 changes: 3 additions & 0 deletions backend/FwLite/LcmCrdt.Tests/Data/MigrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class MigrationTests : IAsyncLifetime
internal static void Init()
{
VerifySystemJson.Initialize();
VerifierSettings.OmitContentFromException();
}

public Task InitializeAsync()
Expand Down Expand Up @@ -54,6 +55,7 @@ public async Task GetEntries_WorksAfterMigrationFromScriptedDb(RegressionTestHel
[Theory]
[InlineData(RegressionTestHelper.RegressionVersion.v1)]
[InlineData(RegressionTestHelper.RegressionVersion.v2)]
[Trait("Category", "Verified")]
public async Task VerifyAfterMigrationFromScriptedDb(RegressionTestHelper.RegressionVersion regressionVersion)
{
await _helper.InitializeAsync(regressionVersion);
Expand Down Expand Up @@ -105,6 +107,7 @@ await Task.WhenAll(
[Theory]
[InlineData(RegressionTestHelper.RegressionVersion.v1)]
[InlineData(RegressionTestHelper.RegressionVersion.v2)]
[Trait("Category", "Verified")]
public async Task VerifyRegeneratedSnapshotsAfterMigrationFromScriptedDb(RegressionTestHelper.RegressionVersion regressionVersion)
{
await _helper.InitializeAsync(regressionVersion);
Expand Down
3 changes: 2 additions & 1 deletion backend/FwLite/LcmCrdt.Tests/Data/RegressionTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ private static string GetFilePath(string name, [CallerFilePath] string sourceFil
public enum RegressionVersion
{
v1,
v2
v2,
v3
}
Comment on lines 73 to 78
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if v3.sql script exists
echo "=== Checking for v3.sql script ==="
fd -t f 'v3.sql' backend/FwLite/LcmCrdt.Tests/

# Check current InlineData attributes in MigrationTests
echo "=== Current InlineData in MigrationTests.cs ==="
rg -n 'InlineData.*RegressionVersion' backend/FwLite/LcmCrdt.Tests/Data/MigrationTests.cs

Repository: sillsdev/languageforge-lexbox

Length of output: 535


Add test coverage for v3 regression version.

The v3 enum value was added to RegressionVersion, but the corresponding v3.sql test data file is missing and MigrationTests.cs has no [InlineData] attributes for v3 in either VerifyAfterMigrationFromScriptedDb or VerifyRegeneratedSnapshotsAfterMigrationFromScriptedDb. Either create the test data and add corresponding test attributes, or remove the v3 enum if it's not yet needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/FwLite/LcmCrdt.Tests/Data/RegressionTestHelper.cs` around lines 73 -
78, The RegressionVersion enum now includes v3 but tests and test data are not
updated; either add a v3.sql test fixture and wiring or remove the enum. To fix:
create the missing backend/FwLite/LcmCrdt.Tests/Data/v3.sql with the expected
scripted DB state, then update MigrationTests.cs to add
[InlineData(RegressionVersion.v3)] to both VerifyAfterMigrationFromScriptedDb
and VerifyRegeneratedSnapshotsAfterMigrationFromScriptedDb, ensuring the test
helpers (e.g., methods that map enum to file names) recognize "v3";
alternatively, if v3 is premature, remove the v3 member from the
RegressionVersion enum to restore consistency.

}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public void CanDeserializeLegacyRegressionData()
}

[Fact]
[Trait("Category", "Verified")]
public async Task RegressionDataUpToDate()
{
var legacyJsonArray = ReadJsonArrayFromFile(GetJsonFilePath("SnapshotDeserializationRegressionData.legacy.verified.txt"));
Expand Down
4 changes: 4 additions & 0 deletions backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,28 @@ public async Task DisposeAsync()
}

[Fact]
[Trait("Category", "Verified")]
public async Task VerifyDbModel()
{
await Verify(_crdtDbContext.Model.ToDebugString(MetadataDebugStringOptions.LongDefault));
}

[Fact]
[Trait("Category", "Verified")]
public async Task VerifyChangeModels()
{
await Verify(_jsonSerializerOptions.GetTypeInfo(typeof(IChange)).PolymorphismOptions);
}

[Fact]
[Trait("Category", "Verified")]
public async Task VerifyIObjectBaseModels()
{
await Verify(_jsonSerializerOptions.GetTypeInfo(typeof(IObjectBase)).PolymorphismOptions);
}

[Fact]
[Trait("Category", "Verified")]
public async Task VerifyIObjectWithIdModels()
{
await Verify(_jsonSerializerOptions.GetTypeInfo(typeof(IObjectWithId)).PolymorphismOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public async Task UpdateEntrySearchTableEnumerable_DoesNotCreateDuplicates()
}

[Fact]
[Trait("Category", "Verified")]
public async Task SearchTableIsUpdatedAutomaticallyOnInsert()
{
var id = Guid.NewGuid();
Expand Down Expand Up @@ -117,6 +118,7 @@ public async Task SearchTableIsUpdatedAutomaticallyOnInsert()
}

[Fact]
[Trait("Category", "Verified")]
public async Task SearchTableIsUpdatedAutomaticallyOnUpdate()
{
var id = Guid.NewGuid();
Expand Down
5 changes: 4 additions & 1 deletion backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics;
using LcmCrdt.MediaServer;
using LcmCrdt.Objects;
using Meziantou.Extensions.Logging.Xunit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -71,9 +72,11 @@ public async Task InitializeAsync(string projectName)
_crdtDbContext = await _services.ServiceProvider.GetRequiredService<IDbContextFactory<LcmCrdtDbContext>>().CreateDbContextAsync();
await _crdtDbContext.Database.OpenConnectionAsync();
//can't use ProjectsService.CreateProject because it opens and closes the db context, this would wipe out the in memory db.
var projectData = new ProjectData("Sena 3", projectName, Guid.NewGuid(), null, Guid.NewGuid());
await CrdtProjectsService.InitProjectDb(_crdtDbContext,
new ProjectData("Sena 3", projectName, Guid.NewGuid(), null, Guid.NewGuid()));
projectData);
await currentProjectService.RefreshProjectData();
await PreDefinedData.PredefinedMorphTypes(_services.ServiceProvider.GetRequiredService<DataModel>(), projectData.ClientId);
if (_seedWs)
{
await Api.CreateWritingSystem(new WritingSystem()
Expand Down
15 changes: 3 additions & 12 deletions backend/FwLite/LcmCrdt.Tests/MiniLcmTests/SortingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,10 @@ public override async Task DisposeAsync()
[InlineData("a", SortField.SearchRelevance)] // non-FTS
public async Task SecondaryOrder_DefaultsToStem(string query, SortField sortField)
{
MorphType[] morphTypes = [
new() { Id = Guid.NewGuid(), Kind = MorphTypeKind.Stem, Name = { ["en"] = "Stem" }, SecondaryOrder = 1 },
new() { Id = Guid.NewGuid(), Kind = MorphTypeKind.BoundStem, Name = { ["en"] = "BoundStem" }, SecondaryOrder = 2 },
new() { Id = Guid.NewGuid(), Kind = MorphTypeKind.Suffix, Name = { ["en"] = "Suffix" }, Postfix = "-", SecondaryOrder = 6 },
];

foreach (var morphType in morphTypes)
await Api.CreateMorphType(morphType);

Entry[] expected = [
new() { Id = Guid.NewGuid(), LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.Unknown }, // SecondaryOrder defaults to Stem = 1
new() { Id = Guid.NewGuid(), LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.BoundStem }, // SecondaryOrder = 2
new() { Id = Guid.NewGuid(), LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.Suffix }, // SecondaryOrder = 6
new() { Id = Guid.NewGuid(), LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.Unknown }, // SecondaryOrder defaults to Stem = 0
new() { Id = Guid.NewGuid(), LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.BoundStem }, // SecondaryOrder = 10
new() { Id = Guid.NewGuid(), LexemeForm = { ["en"] = "aaaa" }, MorphType = MorphTypeKind.Suffix }, // SecondaryOrder = 70
];

var ids = expected.Select(e => e.Id).ToHashSet();
Expand Down
Loading
Loading