Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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 @@ -37,7 +37,9 @@ public void Setup()
_projRepo = new ProjectRepositoryMock();
_wordRepo = new WordRepositoryMock();
_permissionService = new PermissionServiceMock();
_wordService = new WordService(_wordRepo);
var countRepo = new SemanticDomainCountRepositoryMock();
var countService = new SemanticDomainCountService(countRepo, _wordRepo);
_wordService = new WordService(_wordRepo, countService);
_audioController = new AudioController(_wordRepo, _wordService, _permissionService);

_projId = _projRepo.Create(new Project { Name = "AudioControllerTests" }).Result!.Id;
Expand Down
4 changes: 3 additions & 1 deletion Backend.Tests/Controllers/LiftControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ public void Setup()
_speakerRepo = new SpeakerRepositoryMock();
_wordRepo = new WordRepositoryMock();
_liftService = new LiftService(new SemanticDomainRepositoryMock(), _speakerRepo);
_wordService = new WordService(_wordRepo);
var countRepo = new SemanticDomainCountRepositoryMock();
var countService = new SemanticDomainCountService(countRepo, _wordRepo);
_wordService = new WordService(_wordRepo, countService);
_liftController = new LiftController(_wordRepo, _projRepo, new PermissionServiceMock(), _liftService,
new HubContextMock<ExportHub>(), new MockLogger());

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 @@ -34,7 +34,9 @@ public void Setup()
_mergeBlacklistRepo = new MergeBlacklistRepositoryMock();
_mergeGraylistRepo = new MergeGraylistRepositoryMock();
_wordRepo = new WordRepositoryMock();
_wordService = new WordService(_wordRepo);
var countRepo = new SemanticDomainCountRepositoryMock();
var countService = new SemanticDomainCountService(countRepo, _wordRepo);
_wordService = new WordService(_wordRepo, countService);
_mergeService = new MergeService(_mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService);
_mergeController = new MergeController(
_mergeService, new HubContextMock<MergeHub>(), new PermissionServiceMock());
Expand Down
4 changes: 3 additions & 1 deletion Backend.Tests/Controllers/WordControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ public void Dispose()
public void Setup()
{
_wordRepo = new WordRepositoryMock();
_wordService = new WordService(_wordRepo);
var countRepo = new SemanticDomainCountRepositoryMock();
var countService = new SemanticDomainCountService(countRepo, _wordRepo);
_wordService = new WordService(_wordRepo, countService);
_permissionService = new PermissionServiceMock();
_wordController = new WordController(_wordRepo, _wordService, _permissionService);
}
Expand Down
61 changes: 61 additions & 0 deletions Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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 SemanticDomainCountRepositoryMock()
{
_counts = new List<ProjectSemanticDomainCount>();
}

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

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

public Task<ProjectSemanticDomainCount> Create(ProjectSemanticDomainCount count)
{
count.Id = Util.RandString();
_counts.Add(count);
return Task.FromResult(count);
}

public Task<bool> 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 ProjectSemanticDomainCount(projectId, domainId, amount)
{
Id = Util.RandString()
};
_counts.Add(count);
}
else
{
count.Count += amount;
}
return Task.FromResult(true);
}

public Task<bool> DeleteAllCounts(string projectId)
{
var removed = _counts.RemoveAll(c => c.ProjectId == projectId);
return Task.FromResult(removed > 0);
}
}
}
4 changes: 3 additions & 1 deletion Backend.Tests/Services/MergeServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ public void Setup()
_mergeBlacklistRepo = new MergeBlacklistRepositoryMock();
_mergeGraylistRepo = new MergeGraylistRepositoryMock();
_wordRepo = new WordRepositoryMock();
_wordService = new WordService(_wordRepo);
var countRepo = new SemanticDomainCountRepositoryMock();
var countService = new SemanticDomainCountService(countRepo, _wordRepo);
_wordService = new WordService(_wordRepo, countService);
_mergeService = new MergeService(_mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService);
}

Expand Down
218 changes: 218 additions & 0 deletions Backend.Tests/Services/SemanticDomainCountServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
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 IWordRepository _wordRepo = 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();
_wordRepo = new WordRepositoryMock();
_countService = new SemanticDomainCountService(_countRepo, _wordRepo);
}

[Test]
public async Task TestUpdateCountsForWord()
{
var word = new Word
{
ProjectId = ProjId,
Senses = new List<Sense>
{
new()
{
SemanticDomains = new List<SemanticDomain>
{
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.Not.Null);
Assert.That(count1!.Count, Is.EqualTo(1));
Assert.That(count2, Is.Not.Null);
Assert.That(count2!.Count, Is.EqualTo(1));
}

[Test]
public async Task TestUpdateCountsForWords()
{
var words = new List<Word>
{
new()
{
ProjectId = ProjId,
Senses = new List<Sense>
{
new()
{
SemanticDomains = new List<SemanticDomain>
{
new() { Id = DomainId1 }
}
}
}
},
new()
{
ProjectId = ProjId,
Senses = new List<Sense>
{
new()
{
SemanticDomains = new List<SemanticDomain>
{
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.Not.Null);
Assert.That(count1!.Count, Is.EqualTo(2));
Assert.That(count2, Is.Not.Null);
Assert.That(count2!.Count, Is.EqualTo(1));
}

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

var newWord = new Word
{
ProjectId = ProjId,
Senses = new List<Sense>
{
new()
{
SemanticDomains = new List<SemanticDomain>
{
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.Not.Null);
Assert.That(count1!.Count, Is.EqualTo(1)); // Unchanged
Assert.That(count2, Is.Not.Null);
Assert.That(count2!.Count, Is.EqualTo(0)); // Decremented by 1
}

[Test]
public async Task TestMigrateCounts()
{
// Create some frontier words
var word1 = new Word
{
ProjectId = ProjId,
Senses = new List<Sense>
{
new()
{
SemanticDomains = new List<SemanticDomain>
{
new() { Id = DomainId1 }
}
}
}
};

var word2 = new Word
{
ProjectId = ProjId,
Senses = new List<Sense>
{
new()
{
SemanticDomains = new List<SemanticDomain>
{
new() { Id = DomainId1 },
new() { Id = DomainId2 }
}
}
}
};

await _wordRepo.Create(word1);
await _wordRepo.Create(word2);

// Migrate counts
await _countService.MigrateCounts(ProjId);

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

Assert.That(count1, Is.Not.Null);
Assert.That(count1!.Count, Is.EqualTo(2));
Assert.That(count2, Is.Not.Null);
Assert.That(count2!.Count, Is.EqualTo(1));
}

[Test]
public async Task TestMigrateClearsOldCounts()
{
// Add an old count
await _countRepo.Create(new ProjectSemanticDomainCount(ProjId, DomainId1, 99));

// Migrate with no words
await _countService.MigrateCounts(ProjId);

var counts = await _countRepo.GetAllCounts(ProjId);
Assert.That(counts, Is.Empty);
}
}
}
5 changes: 4 additions & 1 deletion Backend.Tests/Services/WordServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Backend.Tests.Services
internal sealed class WordServiceTests
{
private IWordRepository _wordRepo = null!;
private ISemanticDomainCountService _countService = null!;
private IWordService _wordService = null!;

private const string ProjId = "WordServiceTestProjId";
Expand All @@ -21,7 +22,9 @@ internal sealed class WordServiceTests
public void Setup()
{
_wordRepo = new WordRepositoryMock();
_wordService = new WordService(_wordRepo);
var countRepo = new SemanticDomainCountRepositoryMock();
_countService = new SemanticDomainCountService(countRepo, _wordRepo);
_wordService = new WordService(_wordRepo, _countService);
}

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

namespace BackendFramework.Interfaces
{
public interface ISemanticDomainCountRepository
{
Task<ProjectSemanticDomainCount?> GetCount(string projectId, string domainId);
Task<List<ProjectSemanticDomainCount>> GetAllCounts(string projectId);
Task<ProjectSemanticDomainCount> Create(ProjectSemanticDomainCount count);
Task<bool> Increment(string projectId, string domainId, int amount = 1);
Task<bool> DeleteAllCounts(string projectId);
}
}
14 changes: 14 additions & 0 deletions Backend/Interfaces/ISemanticDomainCountService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using BackendFramework.Models;

namespace BackendFramework.Interfaces
{
public interface ISemanticDomainCountService
{
Task UpdateCountsForWord(Word word);
Task UpdateCountsForWords(List<Word> words);
Task UpdateCountsAfterWordUpdate(Word oldWord, Word newWord);
Task MigrateCounts(string projectId);
}
}
Loading