Mehrere Instanzen sollen unabhaengig voneinander laufen. Heute tun sie das nicht: Sie teilen sich Protokollverzeichnis, Sicherungsordner und Sicherungszeitplan. In einer gemeinsamen Startup.log standen abwechselnd Zeilen zweier Instanzen, ohne Kennzeichnung, welche gerade spricht -- im Mehrinstanzbetrieb ist damit jede Fehlersuche Ratearbeit. Genau in dieser Lage ist der Aufhaenger aus #529 entstanden. Fuenf Schluessel wandern aus AppSettings nach InstanceSettings.json: logDirectory, backupDirectory, backupKeepCount, autoBackupEnabled, autoBackupTime. Leer heisst jetzt <Instanz>/Logs beziehungsweise <Instanz>/Backups; ein relativer Wert zaehlt weiterhin gegen das Instanzverzeichnis, nicht gegen das Arbeitsverzeichnis des Prozesses (#375 bleibt gewahrt). autoBackupEnabled und autoBackupTime standen in der Planung nicht, sind aber mitgegangen: Ein Schalter, der die taegliche Sicherung fuer eine Instanz einschaltet und sie damit fuer alle einschaltet, ist derselbe Fehler wie ein Ordner fuer alle -- und zwei von vier Sicherungsfeldern zu verschieben hinterliesse eine Einstellungsseite, auf der die Haelfte anwendungsweit gilt. Keine Uebernahme der Altwerte, aber eine Ansage: SettingsManager. AbandonedKeys meldet die aufgegebenen Schluessel, solange sie noch in Settings.json stehen, und der Start protokolliert einmal, was nicht mehr gilt und wohin diese Instanz stattdessen schreibt. Ein anwendungsweiter Pfad, in alle Instanzen kopiert, waere wieder ein Ordner fuer alle. webServerPort bleibt vorerst stehen. Es entfaellt mit Stufe 2, sobald Kestrel auf dem Socket hoert; vorher wegzunehmen hiesse, die Weboberflaeche ohne Ersatz abzuschalten. Korrektur an der eigenen Befundlage: "Instanz A wirft die Sicherungen von Instanz B weg" war zu breit. Beide Rotationen filtern bereits nach dem Praefix backup_<Instanzname>_. Der reale Defekt ist enger, und ein Praefix ist keine Grenze: backup_Test_2_*.zip faengt mit backup_Test_ an, also zaehlt Test im gemeinsamen Ordner die Archive von Test_2 mit. Belegt in BackupSchedulerTests, das den alten Zustand eigens wiederherstellt. Nachgeprueft an der laufenden Installation, nicht nur im Test: Nach dem Umbau schreibt Instance-Test nach Instances/Instance-Test/Logs, das alte gemeinsame Verzeichnis bekommt keine Zeile mehr, und die Warnung zu den aufgegebenen Schluesseln erscheint genau einmal. Enthaelt ausserdem, was seit der Weboberflaeche liegengeblieben war: - #526: Instanzgeheimnisse wurden beim Speichern verschluesselt und beim Laden nie entschluesselt. LoadInstanceConfig hatte kein ConfigSecrets.Unprotect; ConfigLoader.LoadAsync macht es richtig, wird zur Laufzeit aber gar nicht aufgerufen. Der OpenRouter-Schluessel ging so verschluesselt an die API. - Ein offener Ereignisstrom hielt das Beenden fuenf Sekunden auf. Die Stroeme hingen nur an RequestAborted, das beim geordneten Herunterfahren nicht feuert; jetzt zusaetzlich an ApplicationStopping. Der Fang in WebUiHost gilt dazu ohne Bedingung auf RequestAborted -- mit ihr waere jeder offene Strom beim Beenden als Fehler im Bugtracker gelandet. - Nach der Anmeldung blieb die Weboberflaeche leer: #anmeldung { display:flex } schlaegt als ID-Selektor das hidden-Attribut. - docs/Instanzmanager-Konzept.md: Befund, die Entscheidungen E-M1 bis E-M6, der Schnitt zwischen Manager und Instanz und die sechs Stufen. Build ohne Warnungen, 709 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107 lines
3.5 KiB
C#
107 lines
3.5 KiB
C#
using ClawdDotNet.App.Settings;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.App.Tests.Settings;
|
|
|
|
/// <summary>
|
|
/// Was mit den Einstellungen geschieht, die es hier nicht mehr gibt (#528, Stufe 1).
|
|
///
|
|
/// <para><b>Der Anlass.</b> Fünf Schlüssel sind aus <see cref="AppSettings"/> in die
|
|
/// Instanz gewandert. System.Text.Json überliest unbekannte Schlüssel wortlos, und
|
|
/// <c>Save</c> schreibt sie danach nicht wieder — wer ein eigenes Log- oder
|
|
/// Sicherungsverzeichnis eingetragen hatte, verlöre den Wert also, ohne es zu erfahren.
|
|
/// Ein Umzug ohne Ausweichlogik ist in Ordnung; einer ohne Ansage nicht.</para>
|
|
///
|
|
/// <para>Bewusst keine Übernahme in die Instanzen: Ein anwendungsweiter Pfad, in alle
|
|
/// Instanzen kopiert, wäre wieder derselbe Ordner für alle — also genau der Zustand, den
|
|
/// Stufe 1 beseitigt.</para>
|
|
/// </summary>
|
|
public sealed class SettingsManagerTests : IDisposable
|
|
{
|
|
private readonly string _dir = Path.Combine(
|
|
Path.GetTempPath(), "clawd-tests", "settings-" + Guid.NewGuid().ToString("N")[..8]);
|
|
|
|
private string Datei => Path.Combine(_dir, "Settings.json");
|
|
|
|
public SettingsManagerTests() => Directory.CreateDirectory(_dir);
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(_dir, recursive: true); }
|
|
catch { /* Ein liegengebliebener Testordner faerbt keinen Lauf rot. */ }
|
|
}
|
|
|
|
private SettingsManager Geladen(string json)
|
|
{
|
|
File.WriteAllText(Datei, json);
|
|
var manager = new SettingsManager(_dir);
|
|
manager.Load();
|
|
return manager;
|
|
}
|
|
|
|
[Fact]
|
|
public void Eine_alte_Datei_meldet_ihre_aufgegebenen_Schluessel()
|
|
{
|
|
var manager = Geladen("""
|
|
{
|
|
"logDirectory": "/var/log/clawd",
|
|
"backupDirectory": "/srv/sicherungen",
|
|
"backupKeepCount": 30,
|
|
"autoBackupEnabled": true,
|
|
"autoBackupTime": "02:30",
|
|
"minimumLogLevel": "Debug"
|
|
}
|
|
""");
|
|
|
|
manager.AbandonedKeys.ShouldBe(
|
|
["logDirectory", "backupDirectory", "backupKeepCount",
|
|
"autoBackupEnabled", "autoBackupTime"],
|
|
ignoreOrder: true);
|
|
}
|
|
|
|
[Fact]
|
|
public void Was_die_Datei_sonst_enthaelt_wird_weiter_gelesen()
|
|
{
|
|
// Die Ansage darf nichts daran ändern, dass der Rest ankommt.
|
|
var manager = Geladen("""
|
|
{
|
|
"logDirectory": "/var/log/clawd",
|
|
"minimumLogLevel": "Debug",
|
|
"maxLogLinesInUi": 4000
|
|
}
|
|
""");
|
|
|
|
manager.AppSettings.MinimumLogLevel.ShouldBe("Debug");
|
|
manager.AppSettings.MaxLogLinesInUi.ShouldBe(4000);
|
|
}
|
|
|
|
[Fact]
|
|
public void Eine_frische_Datei_meldet_nichts()
|
|
{
|
|
Geladen("""{ "minimumLogLevel": "Info" }""").AbandonedKeys.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Speichern_wird_die_aufgegebenen_Schluessel_los()
|
|
{
|
|
var manager = Geladen("""{ "logDirectory": "/var/log/clawd" }""");
|
|
manager.Save();
|
|
|
|
File.ReadAllText(Datei).ShouldNotContain("logDirectory");
|
|
|
|
// Und der zweite Ladevorgang hat nichts mehr zu melden.
|
|
var erneut = new SettingsManager(_dir);
|
|
erneut.Load();
|
|
erneut.AbandonedKeys.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Eine_unlesbare_Datei_verhindert_den_Start_nicht()
|
|
{
|
|
var manager = Geladen("{ das ist kein JSON");
|
|
|
|
manager.AppSettings.MinimumLogLevel.ShouldBe("Info");
|
|
manager.AbandonedKeys.ShouldBeEmpty();
|
|
}
|
|
}
|