Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
eea0478
Initial plan
Copilot Dec 15, 2025
2ea49c0
Implement semantic domain count tracking collection
Copilot Dec 16, 2025
7220b09
Add LIFT import integration for semantic domain count updates
Copilot Dec 16, 2025
8bdcd22
Refactor SemanticDomainCountService and simplify WordService deletion…
Copilot Dec 16, 2025
a605dc6
Simplify
imnasnainaec Dec 16, 2025
0fc6780
Condense test objects
imnasnainaec Dec 17, 2025
3ca58d0
Merge branch 'master' into copilot/add-sense-count-collection
imnasnainaec Dec 17, 2025
d610c50
Move from WordController to StatisticsController
imnasnainaec Dec 17, 2025
00b10d7
Condense
imnasnainaec Dec 18, 2025
0adbf00
Remove unused stuff
imnasnainaec Jan 8, 2026
56a131c
Clear domain counts when clearing frontier
imnasnainaec Jan 8, 2026
46bfcaf
Merge branch 'master' into copilot/add-sense-count-collection
imnasnainaec Jan 9, 2026
5ad575b
Merge branch 'master' into copilot/add-sense-count-collection
imnasnainaec Jan 14, 2026
ca760ca
Refactor word repo/service balance
imnasnainaec Jan 14, 2026
a372fac
Merge branch 'master' into copilot/add-sense-count-collection
imnasnainaec Jan 16, 2026
faf07eb
Move sem-dom count updates into WordService
imnasnainaec Jan 16, 2026
b640d60
Tidy
imnasnainaec Jan 16, 2026
2bf374b
Fix bugs found by the bunny
imnasnainaec Jan 20, 2026
d1c44e1
Add unique compound index on ProjectId and DomainId
Copilot Jan 20, 2026
55ff0d4
Respond to the bunny's nitpicks
imnasnainaec Jan 20, 2026
1be8ff7
Merge branch 'copilot/add-sense-count-collection' of https://github.c…
imnasnainaec Jan 20, 2026
9a90281
Update test
imnasnainaec Jan 20, 2026
ccccabe
Respond to the bunny's nitpicks
imnasnainaec Jan 20, 2026
2fc4747
Reimplement compound index
imnasnainaec Jan 21, 2026
7a92e71
Fix DeleteAudio domain count updating
imnasnainaec Jan 21, 2026
7ba3250
Merge branch 'master' into copilot/add-sense-count-collection
imnasnainaec Feb 6, 2026
25eff94
Move domain count api function back from stats to word controller
imnasnainaec Feb 6, 2026
fae569a
Revert out-of-scope change
imnasnainaec Feb 6, 2026
4aecbf4
Merge branch 'master' into copilot/add-sense-count-collection
imnasnainaec Feb 24, 2026
4f555ef
Merge branch 'master' into copilot/add-sense-count-collection
imnasnainaec Feb 24, 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
4 changes: 3 additions & 1 deletion Backend.Tests/Controllers/AudioControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ public void Dispose()
public void Setup()
{
_projRepo = new ProjectRepositoryMock();
var semDomCountRepo = new SemanticDomainCountRepositoryMock();
_wordRepo = new WordRepositoryMock();
_permissionService = new PermissionServiceMock();
_wordService = new WordService(_wordRepo);
var semDomCountService = new SemanticDomainCountService(semDomCountRepo);
_wordService = new WordService(_wordRepo, semDomCountService);
_audioController = new AudioController(_wordRepo, _wordService, _permissionService);

_projId = _projRepo.Create(new Project { Name = "AudioControllerTests" }).Result!.Id;
Expand Down
6 changes: 4 additions & 2 deletions Backend.Tests/Controllers/LiftControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,18 @@ public void Setup()
{
_projRepo = new ProjectRepositoryMock();
var semDomRepo = new SemanticDomainRepositoryMock();
var semDomCountRepo = new SemanticDomainCountRepositoryMock();
_speakerRepo = new SpeakerRepositoryMock();
_wordRepo = new WordRepositoryMock();
var ackService = new AcknowledgmentServiceMock();
_liftService = new LiftService();
var notifyService = new HubContextMock<ExportHub>();
var permissionService = new PermissionServiceMock();
_wordService = new WordService(_wordRepo);
var semDomCountService = new SemanticDomainCountService(semDomCountRepo);
_wordService = new WordService(_wordRepo, semDomCountService);
var logger = new LoggerMock<LiftController>();
_liftController = new LiftController(_projRepo, semDomRepo, _speakerRepo, _wordRepo, ackService,
_liftService, notifyService, permissionService, logger);
_liftService, notifyService, permissionService, _wordService, logger);

_projId = _projRepo.Create(new Project { Name = ProjName }).Result!.Id;
_file = new FormFile(_stream, 0, _stream.Length, "Name", FileName);
Expand Down
4 changes: 3 additions & 1 deletion Backend.Tests/Controllers/MergeControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ public void Setup()
new ServiceCollection().AddMemoryCache().BuildServiceProvider().GetRequiredService<IMemoryCache>();
_mergeBlacklistRepo = new MergeBlacklistRepositoryMock();
_mergeGraylistRepo = new MergeGraylistRepositoryMock();
var semDomCountRepo = new SemanticDomainCountRepositoryMock();
_wordRepo = new WordRepositoryMock();
var ackService = new AcknowledgmentServiceMock();
_wordService = new WordService(_wordRepo);
var semDomCountService = new SemanticDomainCountService(semDomCountRepo);
_wordService = new WordService(_wordRepo, semDomCountService);
_mergeService = new MergeService(_cache, _mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService);
var notifyService = new HubContextMock<MergeHub>();
var permissionService = new PermissionServiceMock();
Expand Down
6 changes: 4 additions & 2 deletions Backend.Tests/Controllers/WordControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ public void Dispose()
[SetUp]
public void Setup()
{
var semDomCountRepo = new SemanticDomainCountRepositoryMock();
_wordRepo = new WordRepositoryMock();
_wordService = new WordService(_wordRepo);
var semDomCountService = new SemanticDomainCountService(semDomCountRepo);
_wordService = new WordService(_wordRepo, semDomCountService);
_permissionService = new PermissionServiceMock();
_wordController = new WordController(_wordRepo, _wordService, _permissionService);
_wordController = new WordController(semDomCountRepo, _wordRepo, _wordService, _permissionService);
}

[Test]
Expand Down
45 changes: 45 additions & 0 deletions Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BackendFramework.Interfaces;
using BackendFramework.Models;

namespace Backend.Tests.Mocks
{
public sealed class SemanticDomainCountRepositoryMock : ISemanticDomainCountRepository
{
private readonly List<ProjectSemanticDomainCount> _counts = [];

public Task<int> GetCount(string projectId, string domainId)
{
var count = _counts.FirstOrDefault(c => c.ProjectId == projectId && c.DomainId == domainId);
return Task.FromResult(count?.Count ?? 0);
}

public Task<List<ProjectSemanticDomainCount>> GetAllCounts(string projectId)
{
var counts = _counts.Where(c => c.ProjectId == projectId).Select(c => c.Clone()).ToList();
return Task.FromResult(counts);
}

public Task<int> Increment(string projectId, string domainId, int amount = 1)
{
var count = _counts.FirstOrDefault(c => c.ProjectId == projectId && c.DomainId == domainId);
if (count is null)
{
count = new(projectId, domainId, amount) { Id = Util.RandString() };
_counts.Add(count);
}
else
{
count.Count += amount;
}
return Task.FromResult(count.Count);
}

public Task<int> DeleteAllCounts(string projectId)
{
return Task.FromResult(_counts.RemoveAll(c => c.ProjectId == projectId));
}
}
}
7 changes: 0 additions & 7 deletions Backend.Tests/Mocks/WordRepositoryMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,5 @@ public Task<Word> Add(Word word)
_words.Add(word.Clone());
return Task.FromResult(word);
}

public Task<int> CountFrontierWordsWithDomain(string projectId, string domainId)
{
var count = _frontier.Count(
w => w.ProjectId == projectId && w.Senses.Any(s => s.SemanticDomains.Any(sd => sd.Id == domainId)));
return Task.FromResult(count);
}
}
}
4 changes: 3 additions & 1 deletion Backend.Tests/Services/MergeServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ public void Setup()
new ServiceCollection().AddMemoryCache().BuildServiceProvider().GetRequiredService<IMemoryCache>();
_mergeBlacklistRepo = new MergeBlacklistRepositoryMock();
_mergeGraylistRepo = new MergeGraylistRepositoryMock();
var semDomCountRepo = new SemanticDomainCountRepositoryMock();
_wordRepo = new WordRepositoryMock();
_wordService = new WordService(_wordRepo);
var semDomCountService = new SemanticDomainCountService(semDomCountRepo);
_wordService = new WordService(_wordRepo, semDomCountService);
_mergeService = new MergeService(_cache, _mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService);
}

Expand Down
125 changes: 125 additions & 0 deletions Backend.Tests/Services/SemanticDomainCountServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Backend.Tests.Mocks;
using BackendFramework.Interfaces;
using BackendFramework.Models;
using BackendFramework.Services;
using NUnit.Framework;

namespace Backend.Tests.Services
{
internal sealed class SemanticDomainCountServiceTests
{
private ISemanticDomainCountRepository _countRepo = null!;
private ISemanticDomainCountService _countService = null!;

private const string ProjId = "CountServiceTestProjId";
private const string DomainId1 = "1.1";
private const string DomainId2 = "2.1";

[SetUp]
public void Setup()
{
_countRepo = new SemanticDomainCountRepositoryMock();
_countService = new SemanticDomainCountService(_countRepo);
}

[Test]
public async Task TestUpdateCountsForWord()
{
var word = new Word
{
ProjectId = ProjId,
Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }, new() { Id = DomainId2 }] }],
};

await _countService.UpdateCountsForWord(word);

var count1 = await _countRepo.GetCount(ProjId, DomainId1);
var count2 = await _countRepo.GetCount(ProjId, DomainId2);

Assert.That(count1, Is.EqualTo(1));
Assert.That(count2, Is.EqualTo(1));
}

[Test]
public async Task TestUpdateCountsForWords()
{
var words = new List<Word>
{
new()
{
ProjectId = ProjId,
Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }] }]
},
new()
{
ProjectId = ProjId,
Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }, new() { Id = DomainId2 }] }],
}
};

await _countService.UpdateCountsForWords(words);

var count1 = await _countRepo.GetCount(ProjId, DomainId1);
var count2 = await _countRepo.GetCount(ProjId, DomainId2);

Assert.That(count1, Is.EqualTo(2));
Assert.That(count2, Is.EqualTo(1));
}

[Test]
public async Task TestUpdateCountsAfterWordUpdate()
{
var oldWord = new Word
{
ProjectId = ProjId,
Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }, new() { Id = DomainId2 }] }],
};

var newWord = new Word
{
ProjectId = ProjId,
Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }] }]
};

// Start with counts from old word
await _countService.UpdateCountsForWord(oldWord);

// Update counts
await _countService.UpdateCountsAfterWordUpdate(oldWord, newWord);

var count1 = await _countRepo.GetCount(ProjId, DomainId1);
var count2 = await _countRepo.GetCount(ProjId, DomainId2);

Assert.That(count1, Is.EqualTo(1)); // Unchanged
Assert.That(count2, Is.EqualTo(0)); // Decremented by 1
}

[Test]
public async Task TestUpdateCountsForWordDeletion()
{
var word = new Word
{
ProjectId = ProjId,
Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }, new() { Id = DomainId2 }] }],
};

// First add the word to get initial counts
await _countService.UpdateCountsForWord(word);

var count1Before = await _countRepo.GetCount(ProjId, DomainId1);
var count2Before = await _countRepo.GetCount(ProjId, DomainId2);
Assert.That(count1Before, Is.EqualTo(1));
Assert.That(count2Before, Is.EqualTo(1));

// Now delete it
await _countService.UpdateCountsForWordDeletion(word);

var count1After = await _countRepo.GetCount(ProjId, DomainId1);
var count2After = await _countRepo.GetCount(ProjId, DomainId2);
Assert.That(count1After, Is.EqualTo(0));
Assert.That(count2After, Is.EqualTo(0));
}
}
}
45 changes: 24 additions & 21 deletions Backend.Tests/Services/StatisticsServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,20 @@ namespace Backend.Tests.Services
{
internal sealed class StatisticsServiceTests
{
private ISemanticDomainRepository _domainRepo = null!;
private ISemanticDomainRepository _semDomRepo = null!;
private ISemanticDomainCountRepository _semDomCountRepo = null!;
private IUserRepository _userRepo = null!;
private IWordRepository _wordRepo = null!;
private IStatisticsService _statsService = null!;

private const string ProjId = "StatsServiceTestProjId";
private const string SemDomId = "StatsServiceTestSemDomId";
private readonly List<DateTime> NonEmptySchedule = new() { DateTime.Now };
private readonly List<SemanticDomainTreeNode> TreeNodes = new() { new(new SemanticDomain { Id = SemDomId }) };
private readonly List<DateTime> NonEmptySchedule = [DateTime.Now];
private readonly List<SemanticDomainTreeNode> TreeNodes = [new(new SemanticDomain { Id = SemDomId })];

private static Sense GetSenseWithDomain(string semDomId = SemDomId)
{
return new() { SemanticDomains = new() { new() { Id = semDomId } } };
return new() { SemanticDomains = [new() { Id = semDomId }] };
}
private static User GetUserWithProjId(string projId = ProjId)
{
Expand All @@ -37,25 +38,26 @@ private static Word GetWordWithDomain(string semDomId = SemDomId)
{
Id = Util.RandString(10),
ProjectId = ProjId,
Senses = new() { GetSenseWithDomain(semDomId) },
Senses = [GetSenseWithDomain(semDomId)],
Vernacular = Util.RandString(10)
};
}

[SetUp]
public void Setup()
{
_domainRepo = new SemanticDomainRepositoryMock();
_semDomRepo = new SemanticDomainRepositoryMock();
_semDomCountRepo = new SemanticDomainCountRepositoryMock();
_userRepo = new UserRepositoryMock();
_wordRepo = new WordRepositoryMock();
_statsService = new StatisticsService(_wordRepo, _domainRepo, _userRepo);
_statsService = new StatisticsService(_semDomRepo, _semDomCountRepo, _userRepo, _wordRepo);
}

[Test]
public void GetSemanticDomainCountsTestNullDomainList()
{
// Add a word to the database and leave the semantic domain list null
_wordRepo.AddFrontier(GetWordWithDomain());
// Add a domain count to the database and leave the semantic domain list null
_semDomCountRepo.Increment(ProjId, SemDomId).Wait();

var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result;
Assert.That(result, Is.Empty);
Expand All @@ -64,30 +66,31 @@ public void GetSemanticDomainCountsTestNullDomainList()
[Test]
public void GetSemanticDomainCountsTestEmptyDomainList()
{
// Add to the database a word and an empty list of semantic domains
((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(new List<SemanticDomainTreeNode>());
_wordRepo.AddFrontier(GetWordWithDomain());
// Add to the database an empty list of semantic domains and a domain count
((SemanticDomainRepositoryMock)_semDomRepo).SetNextResponse(new List<SemanticDomainTreeNode>());
_semDomCountRepo.Increment(ProjId, SemDomId).Wait();

var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result;
Assert.That(result, Is.Empty);
}

[Test]
public void GetSemanticDomainCountsTestEmptyFrontier()
public void GetSemanticDomainCountsTestEmptyCounts()
{
// Add to the database a semantic domain but no word
((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(TreeNodes);
((SemanticDomainRepositoryMock)_semDomRepo).SetNextResponse(TreeNodes);

var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result;
Assert.That(result, Is.Empty);
Assert.That(result, Is.Not.Empty);
Assert.That(result.First().Count, Is.EqualTo(0));
}

[Test]
public void GetSemanticDomainCountsTestIdMismatch()
{
// Add to the database a semantic domain and a word with a different semantic domain
((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(TreeNodes);
_wordRepo.AddFrontier(GetWordWithDomain("different-id"));
// Add to the database a semantic domain and count with a different domain id
((SemanticDomainRepositoryMock)_semDomRepo).SetNextResponse(TreeNodes);
_semDomCountRepo.Increment(ProjId, "DifferentId").Wait();

var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result;
Assert.That(result, Has.Count.EqualTo(1));
Expand All @@ -97,9 +100,9 @@ public void GetSemanticDomainCountsTestIdMismatch()
[Test]
public void GetSemanticDomainCountsTestIdMatch()
{
// Add to the database a semantic domain and a word with the same semantic domain
((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(TreeNodes);
_wordRepo.AddFrontier(GetWordWithDomain());
// Add to the database a semantic domain and a corresponding count
((SemanticDomainRepositoryMock)_semDomRepo).SetNextResponse(TreeNodes);
_semDomCountRepo.Increment(ProjId, SemDomId).Wait();

var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result;
Assert.That(result, Has.Count.EqualTo(1));
Expand Down
4 changes: 3 additions & 1 deletion Backend.Tests/Services/WordServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ internal sealed class WordServiceTests
[SetUp]
public void Setup()
{
var semDomCountRepo = new SemanticDomainCountRepositoryMock();
_wordRepo = new WordRepositoryMock();
_wordService = new WordService(_wordRepo);
var semDomCountService = new SemanticDomainCountService(semDomCountRepo);
_wordService = new WordService(_wordRepo, semDomCountService);
}

[Test]
Expand Down
Loading
Loading