Skip to content
Draft
Show file tree
Hide file tree
Changes from 25 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
8 changes: 5 additions & 3 deletions Backend.Tests/Controllers/LiftControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@ public void Dispose()
public void Setup()
{
_projRepo = new ProjectRepositoryMock();
var semDomCountRepo = new SemanticDomainCountRepositoryMock();
_speakerRepo = new SpeakerRepositoryMock();
_wordRepo = new WordRepositoryMock();
_liftService = new LiftService();
_wordService = new WordService(_wordRepo);
var semDomCountService = new SemanticDomainCountService(semDomCountRepo);
_wordService = new WordService(_wordRepo, semDomCountService);
_liftController = new LiftController(_projRepo, new SemanticDomainRepositoryMock(), _speakerRepo,
_wordRepo, _liftService, new HubContextMock<ExportHub>(), new PermissionServiceMock(),
_wordRepo, _liftService, new HubContextMock<ExportHub>(), new PermissionServiceMock(), _wordService,
new MockLogger());

_projId = _projRepo.Create(new Project { Name = ProjName }).Result!.Id;
Expand Down Expand Up @@ -414,7 +416,7 @@ public async Task TestDeletedWordsExportToLift()
word.Vernacular = "updated";

await _wordService.Update(_projId, UserId, wordToUpdate.Id, word);
await _wordService.DeleteFrontierWord(_projId, UserId, wordToDelete.Id);
await _wordService.MakeFrontierDeleted(_projId, UserId, wordToDelete.Id);

_liftService.SetExportInProgress(UserId, ExportId);
await _liftController.CreateLiftExportThenSignal(_projId, UserId, ExportId);
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,8 +38,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);
var notifyService = new HubContextMock<MergeHub>();
var permissionService = new PermissionServiceMock();
Expand Down
18 changes: 17 additions & 1 deletion Backend.Tests/Controllers/StatisticsControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public async Task Setup()
_projRepo = new ProjectRepositoryMock();
_userRepo = new UserRepositoryMock();
_permService = new PermissionServiceMock(_userRepo);
_statsController = new StatisticsController(new StatisticsServiceMock(), _permService, _projRepo)
_statsController = new StatisticsController(_projRepo, _permService, new StatisticsServiceMock())
{
// Mock the Http Context because this isn't an actual call controller
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
Expand All @@ -46,6 +46,22 @@ public async Task Setup()
_projId = (await _projRepo.Create(new Project { Name = "StatisticsControllerTests" }))!.Id;
}

[Test]
public async Task TestGetDomainCountNoPermission()
{
_statsController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext();

var result = await _statsController.GetDomainCount(_projId, "1");
Assert.That(result, Is.InstanceOf<ForbidResult>());
}

[Test]
public async Task TestGetDomainCount()
{
var result = await _statsController.GetDomainCount(_projId, "1");
Assert.That(result, Is.InstanceOf<OkObjectResult>());
}

[Test]
public async Task TestGetSemanticDomainCountsNoPermission()
{
Expand Down
20 changes: 3 additions & 17 deletions Backend.Tests/Controllers/WordControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ 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);
}
Expand Down Expand Up @@ -410,21 +412,5 @@ public async Task TestRestoreWordMissingWord()
var wordResult = await _wordController.RestoreWord(ProjId, MissingId);
Assert.That(wordResult, Is.InstanceOf<NotFoundResult>());
}

[Test]
public async Task TestGetDomainWordCountNoPermission()
{
_wordController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext();

var result = await _wordController.GetDomainWordCount(ProjId, "1");
Assert.That(result, Is.InstanceOf<ForbidResult>());
}

[Test]
public async Task TestGetDomainWordCount()
{
var result = await _wordController.GetDomainWordCount(ProjId, "1");
Assert.That(result, Is.InstanceOf<OkObjectResult>());
}
}
}
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));
}
}
}
5 changes: 5 additions & 0 deletions Backend.Tests/Mocks/StatisticsServiceMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ namespace Backend.Tests.Mocks
{
internal sealed class StatisticsServiceMock : IStatisticsService
{
public Task<int> GetDomainCount(string projectId, string domainId)
{
return Task.FromResult(0);
}

public Task<List<SemanticDomainCount>> GetSemanticDomainCounts(string projectId, string lang)
{
return Task.FromResult(new List<SemanticDomainCount>());
Expand Down
15 changes: 0 additions & 15 deletions Backend.Tests/Mocks/WordRepositoryMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,26 +139,11 @@ public Task<List<Word>> AddFrontier(List<Word> words)
return Task.FromResult<Word?>(word);
}

public Task<long> DeleteFrontierWords(string projectId, List<string> wordIds)
{
long deletedCount = 0;
wordIds.ForEach(id => deletedCount += _frontier.RemoveAll(
word => word.ProjectId == projectId && word.Id == id));
return Task.FromResult(deletedCount);
}

public Task<Word> Add(Word word)
{
word.Id = Guid.NewGuid().ToString();
_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));
}
}
}
Loading
Loading