using ClawdDotNet.Core.Config; using Shouldly; namespace ClawdDotNet.App.Tests; /// /// Where an instance logs to and backs up to — and that two instances do not end up /// in the same folder (#528, stage 1). /// /// The occasion. Both defaults were application-wide. Startup.log /// therefore contained alternating lines of two instances, without marking which one /// was speaking; and because retention cleans up by prefix in exactly one folder, /// the running instance threw away the other's archives along with its own. As long as that is /// the case, every troubleshooting session in multi-instance operation is guesswork. /// /// What remains from #375. A relative value is resolved against the /// instance directory, not against the working directory of the process. A /// systemd service without WorkingDirectory= starts in /, and "./Logs" /// then pointed to /Logs — not creatable for a service account. /// public sealed class LogDirectoryResolutionTests { private static readonly string InstancePath = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "clawd-tests", "Instance-Test")); private static InstanceConfig Instance( string logDirectory = "", string backupDirectory = "", string? workingDirectory = null) => new() { LogDirectory = logDirectory, BackupDirectory = backupDirectory, WorkingDirectory = workingDirectory ?? InstancePath }; // ─── Defaults ─── [Fact] public void Die_Vorbelegungen_sind_leer() { // The core: empty means "take the folder of this instance". If a value stands here, // every new instance carries it around. new InstanceConfig().LogDirectory.ShouldBe(""); new InstanceConfig().BackupDirectory.ShouldBe(""); } [Fact] public void Ohne_Angabe_protokolliert_die_Instanz_in_ihr_eigenes_Verzeichnis() { AppHost.ResolveLogDirectory(Instance(), InstancePath) .ShouldBe(Path.Combine(InstancePath, "Logs")); } [Fact] public void Ohne_Angabe_sichert_die_Instanz_in_ihr_eigenes_Verzeichnis() { AppHost.ResolveBackupDirectory(Instance(), InstancePath) .ShouldBe(Path.Combine(InstancePath, "Backups")); } /// /// The actual promise of stage 1. Everything else in this file checks the /// mechanics; this test checks the promise. /// [Fact] public void Zwei_Instanzen_ohne_Angabe_teilen_sich_nichts() { var a = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "clawd-tests", "Instance-A")); var b = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "clawd-tests", "Instance-B")); var logA = AppHost.ResolveLogDirectory(Instance(workingDirectory: a), a); var logB = AppHost.ResolveLogDirectory(Instance(workingDirectory: b), b); var backupA = AppHost.ResolveBackupDirectory(Instance(workingDirectory: a), a); var backupB = AppHost.ResolveBackupDirectory(Instance(workingDirectory: b), b); logA.ShouldNotBe(logB); backupA.ShouldNotBe(backupB); } // ─── Entered values ─── [Fact] public void Ein_absoluter_Wert_geht_vor() { var eigenesLog = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "clawd-eigene-logs")); var eigenesBackup = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "clawd-eigene-sicherungen")); AppHost.ResolveLogDirectory(Instance(logDirectory: eigenesLog), InstancePath) .ShouldBe(eigenesLog); AppHost.ResolveBackupDirectory(Instance(backupDirectory: eigenesBackup), InstancePath) .ShouldBe(eigenesBackup); } [Fact] public void Ein_relativer_Wert_zaehlt_gegen_das_Instanzverzeichnis() { // Not against the working directory of the process — that was the error from #375. AppHost.ResolveLogDirectory(Instance(logDirectory: "./Protokolle"), InstancePath) .ShouldBe(Path.Combine(InstancePath, "Protokolle")); } [Fact] public void Ein_relativer_Wert_haengt_nicht_am_Arbeitsverzeichnis_des_Prozesses() { AppHost.ResolveLogDirectory(Instance(logDirectory: "./Protokolle"), InstancePath) .ShouldNotBe(Path.GetFullPath("./Protokolle")); } [Theory] [InlineData("./Logs")] [InlineData("Logs")] public void Die_alte_Vorbelegung_zeigt_auf_dieselbe_Stelle_wie_keine_Angabe(string alt) { // That's why LoadInstanceConfig may turn them into "empty" without anything // shifting. AppHost.ResolveLogDirectory(Instance(logDirectory: alt), InstancePath) .ShouldBe(AppHost.ResolveLogDirectory(Instance(), InstancePath)); } [Fact] public void Nur_Leerzeichen_gelten_als_keine_Angabe() { AppHost.ResolveLogDirectory(Instance(logDirectory: " "), InstancePath) .ShouldBe(Path.Combine(InstancePath, "Logs")); } // ─── Reference point ─── [Fact] public void Ein_leeres_WorkingDirectory_faellt_auf_den_Instanzpfad_zurueck() { // Since CreateInstance the field is in the file as an absolute path, but may have been // emptied by hand. Then the place it was loaded from carries the day. AppHost.ResolveLogDirectory( Instance(logDirectory: "Logs", workingDirectory: ""), InstancePath) .ShouldBe(Path.Combine(InstancePath, "Logs")); } [Fact] public void Ein_relatives_WorkingDirectory_faellt_ebenfalls_auf_den_Instanzpfad_zurueck() { // "./data/" is the old default of the field and would again have been resolved against the // working directory of the process. AppHost.ResolveLogDirectory( Instance(logDirectory: "Logs", workingDirectory: "./data/"), InstancePath) .ShouldBe(Path.Combine(InstancePath, "Logs")); } }