KI-gestützte Planungsunterstützung (4.5.9) + Kompetenzkatalog-Import (8.1.2)
KI-Unterstützung: neuer Einstellungen-Tab (Anmeldung, Guthaben) und Button im Planungs-Tab, der Einheiten+Stunden als JSON an ein neues PHP-Backend (ai-backend/) sendet und die Antwort als prüfbare Vorschlagsliste zurückbringt. Provider-Aufruf, Guthabenverwaltung und Abrechnung nach echten Token-Kosten laufen serverseitig, der Desktop-Client sieht nie einen LLM-API-Key. Zentral abgesichert: eine von der KI zurückgegebene Stunden-Id, die zu keiner echten Lesson der Einheit passt, wird nie als Update übernommen, sondern immer als neue Stunde behandelt. Kompetenzkatalog-Import (8.1.2): JSON-Export/Import für Kompetenzkataloge.
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Tests;
|
||||
|
||||
public sealed class CompetencyCatalogImportServiceTests
|
||||
{
|
||||
private static readonly Guid SubjectId = Guid.NewGuid();
|
||||
|
||||
[Fact]
|
||||
public void Analyze_UngueltigeDatei_VeraendertDenKatalogNicht()
|
||||
{
|
||||
var repository = RepositoryWithExistingCatalog();
|
||||
var service = new CompetencyCatalogImportService(repository);
|
||||
|
||||
Assert.Throws<InvalidDataException>(() =>
|
||||
service.Analyze("{ kaputt", SubjectId, "Mathematik", 7));
|
||||
|
||||
Assert.False(repository.ReplaceWasCalled);
|
||||
Assert.Equal("Alte Beschreibung", repository.Current.Single().Items.Single().Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_ErkenntNeueUnveraenderteUndKonflikte()
|
||||
{
|
||||
var service = new CompetencyCatalogImportService(RepositoryWithExistingCatalog());
|
||||
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
|
||||
|
||||
Assert.Equal(0, preview.NewDomains);
|
||||
Assert.Equal(1, preview.NewCompetencies);
|
||||
Assert.Single(preview.Conflicts);
|
||||
Assert.Equal("item:A1", preview.Conflicts[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_MergeBehaeltBeiKonfliktStandardmaessigDenVorhandenenWert()
|
||||
{
|
||||
var repository = RepositoryWithExistingCatalog();
|
||||
var service = new CompetencyCatalogImportService(repository);
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
|
||||
|
||||
service.Apply(preview, CompetencyCatalogImportMode.Merge);
|
||||
|
||||
Assert.True(repository.ReplaceWasCalled);
|
||||
var items = repository.Current.Single().Items;
|
||||
Assert.Equal("Alte Beschreibung", items.Single(x => x.Code == "A1").Description);
|
||||
Assert.Equal("Neue Kompetenz", items.Single(x => x.Code == "A2").Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_MergeKannImportiertenKonfliktwertUebernehmen()
|
||||
{
|
||||
var repository = RepositoryWithExistingCatalog();
|
||||
var service = new CompetencyCatalogImportService(repository);
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
|
||||
|
||||
service.Apply(preview, CompetencyCatalogImportMode.Merge, new HashSet<string> { "item:A1" });
|
||||
|
||||
Assert.Equal("Importierte Beschreibung",
|
||||
repository.Current.Single().Items.Single(x => x.Code == "A1").Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_NeuerBereichMitVorhandenemCode_ErzeugtKeinenDoppeltenKompetenzcode()
|
||||
{
|
||||
var repository = RepositoryWithExistingCatalog();
|
||||
var service = new CompetencyCatalogImportService(repository);
|
||||
const string json = """
|
||||
{ "domains": [
|
||||
{ "code": "B", "name": "Neuer Bereich", "competencies": [
|
||||
{ "code": "A1", "description": "Andere Fassung" }
|
||||
] }
|
||||
] }
|
||||
""";
|
||||
var preview = service.Analyze(json, SubjectId, "Mathematik", 7);
|
||||
|
||||
service.Apply(preview, CompetencyCatalogImportMode.Merge);
|
||||
|
||||
Assert.Single(repository.Current.SelectMany(x => x.Items), x => x.Code == "A1");
|
||||
Assert.Equal("Alte Beschreibung",
|
||||
repository.Current.SelectMany(x => x.Items).Single(x => x.Code == "A1").Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_ReplaceErsetztDenGesamtenAusgewaehltenKatalog()
|
||||
{
|
||||
var repository = RepositoryWithExistingCatalog();
|
||||
var service = new CompetencyCatalogImportService(repository);
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
|
||||
|
||||
service.Apply(preview, CompetencyCatalogImportMode.Replace);
|
||||
|
||||
Assert.Equal(2, repository.Current.Single().Items.Count);
|
||||
Assert.Equal("Importierte Beschreibung",
|
||||
repository.Current.Single().Items.Single(x => x.Code == "A1").Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_NachZwischenzeitlicherAenderung_VerlangtNeueVorschau()
|
||||
{
|
||||
var repository = RepositoryWithExistingCatalog();
|
||||
var service = new CompetencyCatalogImportService(repository);
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
|
||||
repository.Current[0].UpdatedAt = repository.Current[0].UpdatedAt.AddSeconds(1);
|
||||
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
service.Apply(preview, CompetencyCatalogImportMode.Merge));
|
||||
|
||||
Assert.Contains("Vorschau", exception.Message);
|
||||
Assert.False(repository.ReplaceWasCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_AbweichendesFachUndKlassenstufe_ErzeugtWarnungen()
|
||||
{
|
||||
var service = new CompetencyCatalogImportService(RepositoryWithExistingCatalog());
|
||||
var json = JsonWithConflictAndNewItem()
|
||||
.Replace("Mathematik", "Deutsch")
|
||||
.Replace("\"gradeLevel\": 7", "\"gradeLevel\": 8");
|
||||
|
||||
var preview = service.Analyze(json, SubjectId, "Mathematik", 7);
|
||||
|
||||
Assert.Equal(2, preview.Warnings.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_DoppelterKompetenzcode_WirdAbgelehnt()
|
||||
{
|
||||
var service = new CompetencyCatalogImportService(RepositoryWithExistingCatalog());
|
||||
const string json = """
|
||||
{ "domains": [
|
||||
{ "name": "A", "competencies": [{ "code": "X1", "description": "Eins" }] },
|
||||
{ "name": "B", "competencies": [{ "code": "X1", "description": "Zwei" }] }
|
||||
] }
|
||||
""";
|
||||
|
||||
Assert.Throws<InvalidDataException>(() =>
|
||||
service.Analyze(json, SubjectId, "Mathematik", 7));
|
||||
}
|
||||
|
||||
private static InMemoryCompetencyDomainRepository RepositoryWithExistingCatalog() => new(
|
||||
[
|
||||
new CompetencyDomain
|
||||
{
|
||||
SubjectId = SubjectId,
|
||||
GradeLevel = 7,
|
||||
Code = "A",
|
||||
Name = "Arithmetik",
|
||||
Items =
|
||||
[
|
||||
new CompetencyItem { Code = "A1", Description = "Alte Beschreibung" },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
private static string JsonWithConflictAndNewItem() => """
|
||||
{
|
||||
"subject": "Mathematik",
|
||||
"gradeLevel": 7,
|
||||
"domains": [
|
||||
{
|
||||
"code": "A",
|
||||
"name": "Arithmetik",
|
||||
"competencies": [
|
||||
{ "code": "A1", "description": "Importierte Beschreibung" },
|
||||
{ "code": "A2", "description": "Neue Kompetenz" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
private sealed class InMemoryCompetencyDomainRepository(List<CompetencyDomain> initial)
|
||||
: ICompetencyDomainRepository
|
||||
{
|
||||
public List<CompetencyDomain> Current { get; private set; } = initial;
|
||||
public bool ReplaceWasCalled { get; private set; }
|
||||
|
||||
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
|
||||
Current.Where(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel).ToList();
|
||||
public CompetencyDomain? GetById(Guid id) => Current.FirstOrDefault(x => x.Id == id);
|
||||
|
||||
public void Save(CompetencyDomain domain) => Current.Add(domain);
|
||||
public void Delete(Guid id) => Current.RemoveAll(x => x.Id == id);
|
||||
public void DeleteBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
|
||||
Current.RemoveAll(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel);
|
||||
|
||||
public void ReplaceForSubjectAndGrade(Guid subjectId, int gradeLevel, List<CompetencyDomain> domains)
|
||||
{
|
||||
ReplaceWasCalled = true;
|
||||
Current.RemoveAll(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel);
|
||||
Current.AddRange(domains);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user