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.
199 lines
7.5 KiB
C#
199 lines
7.5 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|