feat: Chemikalien-Recherche über lokale RiSU-Stoffliste (kein KI-Aufruf mehr)

Ersetzt den ursprünglichen KI-Websuche-Ansatz (GESTIS scheiterte sowohl über
Websuche als auch über direkten API-Zugriff an fehlenden schulspezifischen
Tätigkeitsbeschränkungen) durch einen reinen lokalen Datenbank-Lookup gegen die
offizielle RiSU-Stoffliste (1764 Stoffe inkl. Tätigkeitsbeschränkungs-Codes,
Legende zur Abfragezeit übersetzt statt in die Datenbank gebacken). Keine
KI-Kosten, keine Drittanbieter-Abhängigkeit mehr für diese Funktion.

HazardSubstance.AgeRestriction zu ActivityRestriction umbenannt, da die
Tätigkeitsbeschränkung mehr abdeckt als reine Altersgrenzen. Details und
verworfene Zwischenstände in TODO.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 23:43:56 +02:00
co-authored by Claude Sonnet 5
parent 038337997f
commit 7b883555bf
14 changed files with 647 additions and 37 deletions
@@ -1,4 +1,6 @@
using LehrerApp.Core.AiPlanning;
using LehrerApp.Core.Models;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using Xunit;
@@ -8,6 +10,13 @@ public sealed class HazardAssessmentWizardViewModelTests
{
private static HazardAssessmentWizardViewModel Build(HazardAssessment? editing = null) => new(editing, "10c");
private static string BuildTempPath()
{
var path = Path.Combine(Path.GetTempPath(), $"lehrerapp-gbuwizard-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(path);
return path;
}
[Fact]
public void Neu_StartetAufSchritt0OhneKiOhneEditingKennzeichnung()
{
@@ -227,6 +236,111 @@ public sealed class HazardAssessmentWizardViewModelTests
Assert.Equal("Notiz", vm.Notes);
Assert.True(vm.IsAiAssisted);
}
[Fact]
public void CanUseAi_OhneKiAbhaengigkeiten_IstFalseUndSubstanzenKoennenNichtRecherchieren()
{
var vm = Build();
vm.AddSubstanceCommand.Execute(null);
Assert.False(vm.CanUseAi);
Assert.False(vm.Substances[0].CanResearch);
}
[Fact]
public void CanUseAi_MitAngemeldeterKiUndUnitUndLesson_IstTrueUndSubstanzenKoennenRecherchieren()
{
var aiSettings = new AiSettingsService(BuildTempPath());
aiSettings.SetEnabled(true);
aiSettings.SetCredentialsAndToken("sebastian", "token-abc");
var ai = new AiPlanningService(new HttpClient(), new FakeLessons(), new FakeGroups([]),
new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([]));
var unit = new Unit { Title = "Redoxreaktionen" };
var lesson = new Lesson { Topic = "Natrium in Wasser" };
var vm = new HazardAssessmentWizardViewModel(null, "10c", ai, aiSettings, unit, lesson);
vm.AddSubstanceCommand.Execute(null);
Assert.True(vm.CanUseAi);
Assert.True(vm.Substances[0].CanResearch);
}
}
public sealed class HazardSubstanceEditItemTests
{
[Fact]
public async Task Research_OhneOnResearch_TutNichtsUndBleibtOhneFehler()
{
var item = new HazardSubstanceEditItem { Name = "Natrium" };
await item.ResearchCommand.ExecuteAsync(null);
Assert.Equal("", item.ResearchError);
Assert.False(item.IsResearching);
}
[Fact]
public async Task Research_OhneNamen_SetztFehlerUndRuftDelegateNicht()
{
var called = false;
var item = new HazardSubstanceEditItem
{
Name = " ",
OnResearch = _ => { called = true; return Task.FromResult(new AiSubstanceResearchResponse()); },
};
await item.ResearchCommand.ExecuteAsync(null);
Assert.False(called);
Assert.Equal("Stoffname eingeben.", item.ResearchError);
}
[Fact]
public async Task Research_UebernimmtAlleFelderAusDerAntwort()
{
var item = new HazardSubstanceEditItem { Name = "Natrium" };
item.OnResearch = name =>
{
Assert.Equal("Natrium", name);
return Task.FromResult(new AiSubstanceResearchResponse
{
Name = "Natrium",
Cas = "7440-23-5",
SignalWord = "Gefahr",
HStatements = "H260",
PStatements = "P223, P231+P232",
ActivityRestriction = "Für Schüler ab der 5. Klasse freigegeben",
GhsPictograms = ["GHS02", "GHS05"],
});
};
await item.ResearchCommand.ExecuteAsync(null);
Assert.Equal("Natrium", item.Name);
Assert.Equal("7440-23-5", item.Cas);
Assert.Equal("Gefahr", item.SignalWord);
Assert.Equal("H260", item.HStatements);
Assert.Equal("P223, P231+P232", item.PStatements);
Assert.Equal("Für Schüler ab der 5. Klasse freigegeben", item.ActivityRestriction);
Assert.True(item.PictogramOptions.Single(o => o.Value == GhsPictogram.Flammable).IsSelected);
Assert.True(item.PictogramOptions.Single(o => o.Value == GhsPictogram.Corrosive).IsSelected);
Assert.False(item.PictogramOptions.Single(o => o.Value == GhsPictogram.Toxic).IsSelected);
Assert.False(item.IsResearching);
Assert.Equal("", item.ResearchError);
}
[Fact]
public async Task Research_BeiFehler_SetztResearchErrorUndBehaeltAlteFelderUnveraendert()
{
var item = new HazardSubstanceEditItem { Name = "Unbekannt", HStatements = "H999" };
item.OnResearch = _ => throw new AiBackendException("Nicht genügend KI-Guthaben. Bitte Guthaben aufladen.");
await item.ResearchCommand.ExecuteAsync(null);
Assert.Equal("Nicht genügend KI-Guthaben. Bitte Guthaben aufladen.", item.ResearchError);
Assert.Equal("H999", item.HStatements);
Assert.False(item.IsResearching);
}
}
public sealed class ExperimentKindDisplayTests