diff --git a/Darling/Darling.Tests/ManagedConfFileTests.cs b/Darling/Darling.Tests/ManagedConfFileTests.cs index 33017fde4..eacd6d19b 100644 --- a/Darling/Darling.Tests/ManagedConfFileTests.cs +++ b/Darling/Darling.Tests/ManagedConfFileTests.cs @@ -368,6 +368,20 @@ public void RenderBody_ContainsCheckpointTimeout() Assert.Contains("checkpoint_timeout = '15min'", body, StringComparison.Ordinal); } + /// Pin: always renders listen_addresses as + /// exactly loopback (DarlingManagedPostgres.BuildConfAppend's v1 line) — never the network + /// address an exposed store's command line adds. Every verification path that reads this rendered text + /// (, ) + /// depends on that being true — it is the reason the command line always outranks this file for the key, + /// on every exposed store, every start. + [Fact] + public void RenderBody_ListenAddresses_IsAlwaysLoopbackOnly() + { + var body = ManagedConfFile.RenderBody(SampleInputs()); + + Assert.Contains("listen_addresses = '127.0.0.1'", body, StringComparison.Ordinal); + } + /// /// #4246 (claude-desktop's parity pin, 14:47Z): for EVERY marker in /// , every key that marker's OWN legacy builder diff --git a/Darling/Darling.Tests/ManagedConfMigrationRunnerTests.cs b/Darling/Darling.Tests/ManagedConfMigrationRunnerTests.cs index 9eae4c5b3..7bd90cc59 100644 --- a/Darling/Darling.Tests/ManagedConfMigrationRunnerTests.cs +++ b/Darling/Darling.Tests/ManagedConfMigrationRunnerTests.cs @@ -503,6 +503,67 @@ public void VerifyStepB_NewErrorFromManagedFile_Fails() Assert.Contains("work_mem", outcome.MismatchedKeys); } + /// Pin: is the + /// MigratedUnstamped re-verification's own scan (DarlingManagedPostgres.MigrateManagedConfAsync), + /// which must skip the same as + /// does, for the same reason: an exposed store's + /// listen_addresses is always overridden by the command line, so darling-managed.conf's rendered + /// (always loopback-only) line reports error = "setting could not be applied" on every start even + /// though nothing needs healing. 192.0.2.10 (RFC 5737) stands in for the store's own address. + /// + [Fact] + public void FindUnstampedManagedFileErrors_ListenAddressesOverriddenByCommandLine_IsNotAnError() + { + var managedPath = Path.Combine(_dataDir, ManagedConfFile.FileName); + var rows = new List + { + new(SourceFile: managedPath, SourceLine: 1, Name: "listen_addresses", Setting: "127.0.0.1", Applied: false, Error: "setting could not be applied"), + Applied("work_mem", "16MB", file: managedPath, line: 2), + }; + + var (hasError, mismatchedKeys) = ManagedConfMigrationRunner.FindUnstampedManagedFileErrors(rows); + + Assert.False(hasError); + Assert.Empty(mismatchedKeys); + } + + /// Pin: the same skip for port — also command-line-owned + /// (). + [Fact] + public void FindUnstampedManagedFileErrors_PortOverriddenByCommandLine_IsNotAnError() + { + var managedPath = Path.Combine(_dataDir, ManagedConfFile.FileName); + var rows = new List + { + new(SourceFile: managedPath, SourceLine: 1, Name: "port", Setting: "5555", Applied: false, Error: "setting could not be applied"), + Applied("work_mem", "16MB", file: managedPath, line: 2), + }; + + var (hasError, mismatchedKeys) = ManagedConfMigrationRunner.FindUnstampedManagedFileErrors(rows); + + Assert.False(hasError); + Assert.Empty(mismatchedKeys); + } + + /// Pin: a REAL error row on another key (not command-line-owned) still reports as an error, + /// with that key named — the skip is narrow, not a blanket "ignore darling-managed.conf errors". + [Fact] + public void FindUnstampedManagedFileErrors_RealErrorOnOtherKey_StillReportsError() + { + var managedPath = Path.Combine(_dataDir, ManagedConfFile.FileName); + var rows = new List + { + new(SourceFile: managedPath, SourceLine: 1, Name: "listen_addresses", Setting: "127.0.0.1", Applied: false, Error: "setting could not be applied"), + new(SourceFile: managedPath, SourceLine: 2, Name: "work_mem", Setting: "16MB", Applied: false, Error: "invalid value"), + }; + + var (hasError, mismatchedKeys) = ManagedConfMigrationRunner.FindUnstampedManagedFileErrors(rows); + + Assert.True(hasError); + Assert.DoesNotContain("listen_addresses", mismatchedKeys); + Assert.Contains("work_mem", mismatchedKeys); + } + /// Pin: a store with a configured network endpoint always starts PostgreSQL with /// listen_addresses forced onto the command line (). /// The rendered darling-managed.conf line stays loopback-only, so PostgreSQL reports THAT file diff --git a/Darling/PerformanceMonitor.Darling.Service/DarlingManagedPostgres.cs b/Darling/PerformanceMonitor.Darling.Service/DarlingManagedPostgres.cs index d9accae73..dc312314a 100644 --- a/Darling/PerformanceMonitor.Darling.Service/DarlingManagedPostgres.cs +++ b/Darling/PerformanceMonitor.Darling.Service/DarlingManagedPostgres.cs @@ -3037,23 +3037,15 @@ outcome is now unknown. Keep the prior non-null outcome rather than erasing it w same here: migrated, no pending file, stale stamp. Re-verify against what is on disk NOW: no new error row may come from darling-managed.conf relative to the file's own current bytes — the file itself is the ground truth once no - pending snapshot survives to compare against. */ + pending snapshot survives to compare against. Except DarlingStoreHostProfile.CommandLineOnlyKeys + (port, listen_addresses): an exposed store always starts PostgreSQL with both forced onto + the pg_ctl command line, which outranks the file unconditionally, so the rendered + (always loopback-only) listen_addresses line reports an error row here on every start of + an exposed store even though nothing is actually wrong — same trap and same fix as + ManagedConfMigrationRunner.VerifyStepB below. */ var rows = await snapshot(cancellationToken); var managedConfPath = Path.Combine(_dataDirectory, ManagedConfFile.FileName); - var newErrorFromManagedFile = false; - var mismatchedKeys = new List(); - foreach (var row in rows) - { - if (row.Error is not null && row.SourceFile is not null - && string.Equals(Path.GetFileName(row.SourceFile), ManagedConfFile.FileName, StringComparison.OrdinalIgnoreCase)) - { - newErrorFromManagedFile = true; - if (row.Name is not null) - { - mismatchedKeys.Add(row.Name); - } - } - } + var (newErrorFromManagedFile, mismatchedKeys) = ManagedConfMigrationRunner.FindUnstampedManagedFileErrors(rows); if (newErrorFromManagedFile) { diff --git a/Darling/PerformanceMonitor.Darling.Service/ManagedConfMigrationRunner.cs b/Darling/PerformanceMonitor.Darling.Service/ManagedConfMigrationRunner.cs index 42378eadd..847471d52 100644 --- a/Darling/PerformanceMonitor.Darling.Service/ManagedConfMigrationRunner.cs +++ b/Darling/PerformanceMonitor.Darling.Service/ManagedConfMigrationRunner.cs @@ -296,6 +296,49 @@ private static ManagedConfMigrationOutcome CompareAndFinish( ManagedConfVerificationStatus.Failed, mismatches, backupPath, ManagedConfMigrationStep.A); } + /// + /// The re-verification's own scan of a + /// fresh pg_file_settings snapshot: every error row whose sourcefile names + /// , by (not a suffix + /// match — a stray old-darling-managed.conf in an include directory is a different file), EXCEPT + /// (port, listen_addresses) — the + /// same skip applies, and for the same reason: an exposed store's command line + /// always outranks the file for both keys, so PostgreSQL reports the rendered (always loopback-only) file + /// row with error = 'setting could not be applied' on every start, never a real mismatch. Returns + /// the matched keys (empty when clean, whether because nothing matched or every match was named for a + /// skipped key) so the caller can log and build the Failed outcome exactly as before this method + /// existed — this is a pure extraction, not a behavior change beyond the skip. HasError is true + /// whenever a genuine (non-skipped) error row was found, INCLUDING one with no Name — the + /// pre-extraction code failed on that row too, even though it never had a key to name. + /// + internal static (bool HasError, IReadOnlyList MismatchedKeys) FindUnstampedManagedFileErrors( + IReadOnlyList rows) + { + var hasError = false; + var mismatchedKeys = new List(); + foreach (var row in rows) + { + if (row.Error is null || row.SourceFile is null + || !string.Equals(Path.GetFileName(row.SourceFile), ManagedConfFile.FileName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (row.Name is not null && Array.IndexOf(DarlingStoreHostProfile.CommandLineOnlyKeys, row.Name) >= 0) + { + continue; + } + + hasError = true; + if (row.Name is not null) + { + mismatchedKeys.Add(row.Name); + } + } + + return (hasError, mismatchedKeys); + } + /// /// Step B: after a normal derivation has already rendered and written /// darling-managed.conf, checks that (a fresh pg_file_settings