From e893b6ea250a330d5cfc6d14191ddd9332bb1c6b Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Sun, 27 Sep 2026 03:40:32 -0400 Subject: [PATCH 1/4] The PostgreSQL 17 cap tests expect the managed-conf behavior The gated PostgreSQL 17 maintenance_work_mem cap test simulated a pre-#3909 build by appending a bare, unmarked line to postgresql.conf after the store's conf had already migrated to the one included darling-managed.conf file (#4336). No build of this service has ever written that setting bare; every write (v3, v7, v8, v14) carries its own marker. So the appended line was, by the migration classifier's own rule, an operator's line below the include -- and the product now correctly refuses to start on it rather than silently absorb it. Two gated facts replace the one: - Postgres17Store_WithTheOldValueInItsManagedConf_StartsWithTheCap_Gated puts the old, uncapped value directly into darling-managed.conf (the shape the file can carry) and asserts the second start comes up with the value capped, exactly one assignment in the managed file, and no maintenance_work_mem line in postgresql.conf itself. - Postgres17Store_OperatorLineBelowIncludeOutOfRange_RefusesToStartNamingTheFix_Gated appends the bare operator line below the include and asserts the second start throws, naming the setting and the fix, and never leaves the store running. --- .../DarlingManagedPostgresTests.cs | 136 ++++++++++++++++-- 1 file changed, 128 insertions(+), 8 deletions(-) diff --git a/Darling/Darling.Tests/DarlingManagedPostgresTests.cs b/Darling/Darling.Tests/DarlingManagedPostgresTests.cs index 8ef3ff875..fdd09f134 100644 --- a/Darling/Darling.Tests/DarlingManagedPostgresTests.cs +++ b/Darling/Darling.Tests/DarlingManagedPostgresTests.cs @@ -1292,14 +1292,19 @@ public void LegacyMaintenanceWorkMemHeal_RunsBeforeTheStoreUpgradeCanStartTheOld } /// - /// #3909 through the product's own bootstrap. A store on PostgreSQL 17 whose conf ends with the old 2048 MB - /// value (the shape a reverted upgrade left on a 40 GB+ host) comes back up through - /// with the capped value in force. The 17 runtime is - /// the runtime here, with no package beside it, so nothing is upgraded: this is the store that stays on 17. - /// Gated on DARLING_TEST_PGRUNTIME_OLD. + /// #3909 through the product's own bootstrap, after #4336 moved the settings into one included file. A + /// store on PostgreSQL 17 whose darling-managed.conf still carries the old, uncapped 2048 MB value + /// (the shape a store upgraded to #4336 without a fresh render since, or a reverted upgrade, could leave + /// on a 40 GB+ host) comes back up through with + /// the value capped, because every start re-renders darling-managed.conf + /// () and its v14 block + /// () caps any value over PostgreSQL + /// 17's limit on every major at or below 17 — not only the value the render's own formula would derive. + /// The 17 runtime is the runtime here, with no package beside it, so nothing is upgraded: this is the + /// store that stays on 17. Gated on DARLING_TEST_PGRUNTIME_OLD. /// [Fact] - public async Task Postgres17Store_WithTheOldCapInItsConf_StartsThroughTheBootstrap_Gated() + public async Task Postgres17Store_WithTheOldValueInItsManagedConf_StartsWithTheCap_Gated() { var oldRuntime = Environment.GetEnvironmentVariable("DARLING_TEST_PGRUNTIME_OLD"); Assert.SkipWhen(string.IsNullOrWhiteSpace(oldRuntime), @@ -1320,6 +1325,7 @@ public async Task Postgres17Store_WithTheOldCapInItsConf_StartsThroughTheBootstr var dataDirectory = Path.Combine(root.FullName, "store", "pg"); var confPath = Path.Combine(dataDirectory, "postgresql.conf"); + var managedConfPath = Path.Combine(dataDirectory, ManagedConfFile.FileName); var config = new PostgresConfig { Managed = true, Port = FindFreeTcpPort(), DataDirectory = dataDirectory }; using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(8)); DarlingManagedPostgres? first = null; @@ -1331,7 +1337,17 @@ public async Task Postgres17Store_WithTheOldCapInItsConf_StartsThroughTheBootstr await first.StopIfStartedByThisProcessAsync(); Assert.Equal(17, DarlingStoreUpgrade.TryReadDataDirectoryMajor(dataDirectory)); - await File.AppendAllTextAsync(confPath, "\n# written by a build before #3909\nmaintenance_work_mem = 2048MB\n", timeout.Token); + /* The shape a store carrying the old, pre-cap value could still have in its managed file: replace + the ONE maintenance_work_mem assignment the first start's own render just wrote (WriteManagedConfFile + caps on <=17, so it is already at the capped value here) with the old, uncapped 2048MB, keeping + exactly one assignment. */ + var managedTextBefore = await File.ReadAllTextAsync(managedConfPath, timeout.Token); + Assert.Equal(1, CountOccurrences(managedTextBefore, "maintenance_work_mem = ")); + var managedTextWithOldValue = System.Text.RegularExpressions.Regex.Replace( + managedTextBefore, + @"maintenance_work_mem = '[^']*'", + "maintenance_work_mem = '2048MB'"); + await File.WriteAllTextAsync(managedConfPath, managedTextWithOldValue, timeout.Token); second = new DarlingManagedPostgres(config, NullLogger.Instance, runtimeRoot); var connectionString = await second.EnsureRunningAsync(timeout.Token); @@ -1339,7 +1355,111 @@ public async Task Postgres17Store_WithTheOldCapInItsConf_StartsThroughTheBootstr var (live, expected) = await ReadSettingAndLiteralBytesAsync( connectionString, "maintenance_work_mem", $"{DarlingManagedPostgres.MaintenanceWorkMemCapMb}MB", timeout.Token); Assert.Equal(expected, live); - Assert.Equal(1, CountOccurrences(await File.ReadAllTextAsync(confPath, timeout.Token), DarlingManagedPostgres.ConfMarkerV14)); + + var managedTextAfter = await File.ReadAllTextAsync(managedConfPath, timeout.Token); + Assert.Equal(1, CountOccurrences(managedTextAfter, "maintenance_work_mem = ")); + Assert.Contains( + $"maintenance_work_mem = '{DarlingManagedPostgres.MaintenanceWorkMemCapMb}MB'", managedTextAfter, StringComparison.Ordinal); + + var postgresqlConfText = await File.ReadAllTextAsync(confPath, timeout.Token); + Assert.Contains(ManagedConfFile.IncludeLine, postgresqlConfText, StringComparison.Ordinal); + Assert.DoesNotContain("maintenance_work_mem", postgresqlConfText, StringComparison.Ordinal); + } + finally + { + if (first is not null) + { + await first.StopIfStartedByThisProcessAsync(); + } + + if (second is not null) + { + await second.StopIfStartedByThisProcessAsync(); + } + + try + { + root.Delete(recursive: true); + } + catch (IOException) + { + /* A temp directory the OS still holds is not this test's failure. */ + } + } + } + + /// + /// #4405: an operator's own line below the managed include, out of PostgreSQL 17's range, is the + /// operator's line to fix — the store must refuse to start rather than silently absorb it, and its + /// refusal must name the fix. Unlike a value inside darling-managed.conf (the sibling fact above), + /// this line is NEVER something the product wrote: every start re-renders darling-managed.conf + /// itself (), and no code path in this service + /// ever appends a bare maintenance_work_mem line to postgresql.conf below the include — the + /// only appends that ever wrote it (v3, v7, v8, v14) always carry their own marker, and #4336 confined all + /// of them to a Legacy (pre-migration) conf. So a bare, unmarked line after the include is an + /// operator edit, and validates the whole + /// merged conf with postgres -C before ever starting the server: PostgreSQL rejects the value, + /// there is no earlier last-good file to fall back to (this data directory has never had one), and the + /// start throws naming the fix + /// () instead of starting on + /// a value the render never sanctioned. Gated on DARLING_TEST_PGRUNTIME_OLD. + /// + [Fact] + public async Task Postgres17Store_OperatorLineBelowIncludeOutOfRange_RefusesToStartNamingTheFix_Gated() + { + var oldRuntime = Environment.GetEnvironmentVariable("DARLING_TEST_PGRUNTIME_OLD"); + Assert.SkipWhen(string.IsNullOrWhiteSpace(oldRuntime), + "Set DARLING_TEST_PGRUNTIME_OLD to an assembled PostgreSQL 17 pg-runtime (new-upgraded-store-fixture.ps1 builds one)."); + Assert.SkipUnless(OperatingSystem.IsWindows(), "The bundled runtime is Windows-only."); + Assert.SkipUnless(File.Exists(Path.Combine(oldRuntime!, "pgsql", "bin", "pg_ctl.exe")), + $"DARLING_TEST_PGRUNTIME_OLD={oldRuntime} does not contain pgsql\\bin\\pg_ctl.exe."); + + var root = Directory.CreateTempSubdirectory("darling-pg17op-"); + var runtimeRoot = Path.Combine(root.FullName, "deploy", "pg-runtime"); + var source = Path.Combine(oldRuntime!, "pgsql"); + foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories)) + { + var target = Path.Combine(runtimeRoot, "pgsql", Path.GetRelativePath(source, file)); + Directory.CreateDirectory(Path.GetDirectoryName(target)!); + File.Copy(file, target); + } + + var dataDirectory = Path.Combine(root.FullName, "store", "pg"); + var confPath = Path.Combine(dataDirectory, "postgresql.conf"); + var config = new PostgresConfig { Managed = true, Port = FindFreeTcpPort(), DataDirectory = dataDirectory }; + using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(8)); + DarlingManagedPostgres? first = null; + DarlingManagedPostgres? second = null; + try + { + first = new DarlingManagedPostgres(config, NullLogger.Instance, runtimeRoot); + await first.EnsureRunningAsync(timeout.Token); + await first.StopIfStartedByThisProcessAsync(); + Assert.Equal(17, DarlingStoreUpgrade.TryReadDataDirectoryMajor(dataDirectory)); + Assert.Contains(ManagedConfFile.IncludeLine, await File.ReadAllTextAsync(confPath, timeout.Token), StringComparison.Ordinal); + + /* An operator's own line below the include -- never something this product wrote (see the + summary above). PostgreSQL 17 rejects it outright. */ + await File.AppendAllTextAsync(confPath, "\n# an operator's own line\nmaintenance_work_mem = 2048MB\n", timeout.Token); + + second = new DarlingManagedPostgres(config, NullLogger.Instance, runtimeRoot); + var thrown = await Assert.ThrowsAsync( + async () => await second.EnsureRunningAsync(timeout.Token)); + + Assert.Contains("maintenance_work_mem", thrown.Message, StringComparison.Ordinal); + Assert.Contains( + "Fix the rejected setting with a line after 'include ''darling-managed.conf'''", + thrown.Message, StringComparison.Ordinal); + + /* Not left running on the rejected conf: stopping is safe (there is nothing to stop), and a + connection to the configured port fails. */ + await second.StopIfStartedByThisProcessAsync(); + await Assert.ThrowsAnyAsync(async () => + { + await using var connection = new NpgsqlConnection( + $"Host=127.0.0.1;Port={config.Port};Username=darling;Database=postgres;Pooling=false;Timeout=5"); + await connection.OpenAsync(timeout.Token); + }); } finally { From d38a4cf1f0a76bd2845d6a86da874635d52694ca Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Sun, 27 Sep 2026 03:48:26 -0400 Subject: [PATCH 2/4] Keep the managed-conf cap test on the re-render path --- .../DarlingManagedPostgresTests.cs | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/Darling/Darling.Tests/DarlingManagedPostgresTests.cs b/Darling/Darling.Tests/DarlingManagedPostgresTests.cs index fdd09f134..7354b39f1 100644 --- a/Darling/Darling.Tests/DarlingManagedPostgresTests.cs +++ b/Darling/Darling.Tests/DarlingManagedPostgresTests.cs @@ -1340,18 +1340,37 @@ public async Task Postgres17Store_WithTheOldValueInItsManagedConf_StartsWithTheC /* The shape a store carrying the old, pre-cap value could still have in its managed file: replace the ONE maintenance_work_mem assignment the first start's own render just wrote (WriteManagedConfFile caps on <=17, so it is already at the capped value here) with the old, uncapped 2048MB, keeping - exactly one assignment. */ + exactly one assignment. Then recompute the header's body hash over the edited body, the same way + WriteManagedConfFile itself would, so the file still reads as the product's own render (this is + simulating a store that carried this value in a file THE PRODUCT rendered -- e.g. rendered on + PostgreSQL 18 and then reverted to 17 -- not an operator hand edit, which would only ever reach + the last-good fallback in EnsureManagedConfReadyAsync, never the re-render this fact pins). */ var managedTextBefore = await File.ReadAllTextAsync(managedConfPath, timeout.Token); Assert.Equal(1, CountOccurrences(managedTextBefore, "maintenance_work_mem = ")); - var managedTextWithOldValue = System.Text.RegularExpressions.Regex.Replace( - managedTextBefore, + var parsedBefore = ManagedConfFile.ParseExisting(managedTextBefore); + Assert.True(parsedBefore.IsWellFormed); + var bodyWithOldValue = System.Text.RegularExpressions.Regex.Replace( + parsedBefore.Body, @"maintenance_work_mem = '[^']*'", "maintenance_work_mem = '2048MB'"); + /* Rewrite only the header's body-sha256 line to match the edited body -- the rest of the header + (formula version, RAM, CPUs, ...) stays exactly what the first start's own render wrote. This is + what makes the edited file read as the product's own render rather than a hand edit: see the + remarks above. */ + var headerBeforeHashLine = managedTextBefore[..managedTextBefore.IndexOf(ManagedConfFile.BodyHashPrefix, StringComparison.Ordinal)]; + var managedTextWithOldValue = headerBeforeHashLine + ManagedConfFile.BodyHashPrefix + ManagedConfFile.ComputeBodyHash(bodyWithOldValue) + "\n" + bodyWithOldValue; + Assert.False(ManagedConfFile.IsHandEdited(managedTextWithOldValue)); await File.WriteAllTextAsync(managedConfPath, managedTextWithOldValue, timeout.Token); second = new DarlingManagedPostgres(config, NullLogger.Instance, runtimeRoot); var connectionString = await second.EnsureRunningAsync(timeout.Token); + /* The second start re-rendered the file rather than falling back to a last-good copy: this is what + distinguishes this fact from an operator hand edit (the sibling fact below), which the last-good + fallback path would have to handle instead. */ + Assert.True(second.LastManagedConfWriteResult is { Written: true, HandEdited: false }); + Assert.False(second.LastStartUsedLastGoodManagedConf); + var (live, expected) = await ReadSettingAndLiteralBytesAsync( connectionString, "maintenance_work_mem", $"{DarlingManagedPostgres.MaintenanceWorkMemCapMb}MB", timeout.Token); Assert.Equal(expected, live); @@ -1398,9 +1417,14 @@ exactly one assignment. */ /// only appends that ever wrote it (v3, v7, v8, v14) always carry their own marker, and #4336 confined all /// of them to a Legacy (pre-migration) conf. So a bare, unmarked line after the include is an /// operator edit, and validates the whole - /// merged conf with postgres -C before ever starting the server: PostgreSQL rejects the value, - /// there is no earlier last-good file to fall back to (this data directory has never had one), and the - /// start throws naming the fix + /// merged conf with postgres -C before ever starting the server: PostgreSQL rejects the value. + /// The first start above already succeeded, so + /// saved a last-good copy of darling-managed.conf after it — this data directory does have one, and + /// restores and re-validates it. That still + /// fails, because the rejected line lives in postgresql.conf below the include, not in + /// darling-managed.conf: restoring the last-good managed file leaves the operator's own line in + /// place untouched. So the fallback is tried and still refused, and the start throws + /// naming the fix /// () instead of starting on /// a value the render never sanctioned. Gated on DARLING_TEST_PGRUNTIME_OLD. /// @@ -1438,6 +1462,11 @@ public async Task Postgres17Store_OperatorLineBelowIncludeOutOfRange_RefusesToSt Assert.Equal(17, DarlingStoreUpgrade.TryReadDataDirectoryMajor(dataDirectory)); Assert.Contains(ManagedConfFile.IncludeLine, await File.ReadAllTextAsync(confPath, timeout.Token), StringComparison.Ordinal); + /* The first start above succeeded, so it saved a last-good managed-conf copy (design step 2 + -- see the summary above): this data directory does have one to fall back to. */ + var lastGoodPath = Path.Combine(dataDirectory, ManagedConfFile.LastGoodFileName); + Assert.True(File.Exists(lastGoodPath), $"Expected the first start to have saved {lastGoodPath}."); + /* An operator's own line below the include -- never something this product wrote (see the summary above). PostgreSQL 17 rejects it outright. */ await File.AppendAllTextAsync(confPath, "\n# an operator's own line\nmaintenance_work_mem = 2048MB\n", timeout.Token); From 52f73869cb7b719c09fd542742a7903dcaaa4a97 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Sun, 27 Sep 2026 06:17:20 -0400 Subject: [PATCH 3/4] Read the managed-conf cap test's expected value from the render, not the cap --- .../DarlingManagedPostgresTests.cs | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/Darling/Darling.Tests/DarlingManagedPostgresTests.cs b/Darling/Darling.Tests/DarlingManagedPostgresTests.cs index 7354b39f1..2259f2f54 100644 --- a/Darling/Darling.Tests/DarlingManagedPostgresTests.cs +++ b/Darling/Darling.Tests/DarlingManagedPostgresTests.cs @@ -1295,11 +1295,13 @@ public void LegacyMaintenanceWorkMemHeal_RunsBeforeTheStoreUpgradeCanStartTheOld /// #3909 through the product's own bootstrap, after #4336 moved the settings into one included file. A /// store on PostgreSQL 17 whose darling-managed.conf still carries the old, uncapped 2048 MB value /// (the shape a store upgraded to #4336 without a fresh render since, or a reverted upgrade, could leave - /// on a 40 GB+ host) comes back up through with - /// the value capped, because every start re-renders darling-managed.conf - /// () and its v14 block - /// () caps any value over PostgreSQL - /// 17's limit on every major at or below 17 — not only the value the render's own formula would derive. + /// on a 40 GB+ host) comes back up through on a + /// value PostgreSQL 17 accepts, because every start re-renders darling-managed.conf + /// () from this host's own sizing, which on a + /// major at or below 17 never exceeds . The + /// value is host-dependent (a small host renders less than the cap), so the fact reads the render's own + /// line back and asserts the server runs on exactly that value, and that it is at or under the cap; the + /// cap arithmetic itself is pinned without a server by ManagedConfFileTests. /// The 17 runtime is the runtime here, with no package beside it, so nothing is upgraded: this is the /// store that stays on 17. Gated on DARLING_TEST_PGRUNTIME_OLD. /// @@ -1338,8 +1340,8 @@ public async Task Postgres17Store_WithTheOldValueInItsManagedConf_StartsWithTheC Assert.Equal(17, DarlingStoreUpgrade.TryReadDataDirectoryMajor(dataDirectory)); /* The shape a store carrying the old, pre-cap value could still have in its managed file: replace - the ONE maintenance_work_mem assignment the first start's own render just wrote (WriteManagedConfFile - caps on <=17, so it is already at the capped value here) with the old, uncapped 2048MB, keeping + the ONE maintenance_work_mem assignment the first start's own render just wrote (this host's own value, + at or under the cap on <=17) with the old, uncapped 2048MB, keeping exactly one assignment. Then recompute the header's body hash over the edited body, the same way WriteManagedConfFile itself would, so the file still reads as the product's own render (this is simulating a store that carried this value in a file THE PRODUCT rendered -- e.g. rendered on @@ -1371,14 +1373,22 @@ fallback path would have to handle instead. */ Assert.True(second.LastManagedConfWriteResult is { Written: true, HandEdited: false }); Assert.False(second.LastStartUsedLastGoodManagedConf); - var (live, expected) = await ReadSettingAndLiteralBytesAsync( - connectionString, "maintenance_work_mem", $"{DarlingManagedPostgres.MaintenanceWorkMemCapMb}MB", timeout.Token); - Assert.Equal(expected, live); - + /* The re-render replaced the old line with this host's own value. That value depends on the host's + RAM (a CI runner renders less than the cap), so read it back from the file rather than assume + the cap: the server must run on exactly the rendered value, and that value must be one + PostgreSQL 17 accepts. */ var managedTextAfter = await File.ReadAllTextAsync(managedConfPath, timeout.Token); Assert.Equal(1, CountOccurrences(managedTextAfter, "maintenance_work_mem = ")); - Assert.Contains( - $"maintenance_work_mem = '{DarlingManagedPostgres.MaintenanceWorkMemCapMb}MB'", managedTextAfter, StringComparison.Ordinal); + Assert.DoesNotContain("maintenance_work_mem = '2048MB'", managedTextAfter, StringComparison.Ordinal); + var renderedValue = System.Text.RegularExpressions.Regex.Match( + managedTextAfter, "maintenance_work_mem = '([^']*)'").Groups[1].Value; + Assert.False(string.IsNullOrEmpty(renderedValue), "expected the re-rendered darling-managed.conf to carry a maintenance_work_mem value"); + + var (live, expected) = await ReadSettingAndLiteralBytesAsync( + connectionString, "maintenance_work_mem", renderedValue, timeout.Token); + Assert.Equal(expected, live); + Assert.True(live <= DarlingManagedPostgres.MaintenanceWorkMemCapMb * 1024L * 1024L, + $"expected the rendered maintenance_work_mem ({renderedValue}) to be at or under the {DarlingManagedPostgres.MaintenanceWorkMemCapMb} MB cap on PostgreSQL 17"); var postgresqlConfText = await File.ReadAllTextAsync(confPath, timeout.Token); Assert.Contains(ManagedConfFile.IncludeLine, postgresqlConfText, StringComparison.Ordinal); From 4a471b6717dc207a17899067ac955fe403827057 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Sun, 27 Sep 2026 06:43:42 -0400 Subject: [PATCH 4/4] Read the PostgreSQL 17 cap tests' conf lines as active assignments, not substrings --- .../DarlingManagedPostgresTests.cs | 61 ++++++++++++++++--- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/Darling/Darling.Tests/DarlingManagedPostgresTests.cs b/Darling/Darling.Tests/DarlingManagedPostgresTests.cs index 2259f2f54..9e5428800 100644 --- a/Darling/Darling.Tests/DarlingManagedPostgresTests.cs +++ b/Darling/Darling.Tests/DarlingManagedPostgresTests.cs @@ -1348,7 +1348,7 @@ simulating a store that carried this value in a file THE PRODUCT rendered -- e.g PostgreSQL 18 and then reverted to 17 -- not an operator hand edit, which would only ever reach the last-good fallback in EnsureManagedConfReadyAsync, never the re-render this fact pins). */ var managedTextBefore = await File.ReadAllTextAsync(managedConfPath, timeout.Token); - Assert.Equal(1, CountOccurrences(managedTextBefore, "maintenance_work_mem = ")); + Assert.Single(ActiveValues(managedTextBefore, "maintenance_work_mem")); var parsedBefore = ManagedConfFile.ParseExisting(managedTextBefore); Assert.True(parsedBefore.IsWellFormed); var bodyWithOldValue = System.Text.RegularExpressions.Regex.Replace( @@ -1362,6 +1362,7 @@ remarks above. */ var headerBeforeHashLine = managedTextBefore[..managedTextBefore.IndexOf(ManagedConfFile.BodyHashPrefix, StringComparison.Ordinal)]; var managedTextWithOldValue = headerBeforeHashLine + ManagedConfFile.BodyHashPrefix + ManagedConfFile.ComputeBodyHash(bodyWithOldValue) + "\n" + bodyWithOldValue; Assert.False(ManagedConfFile.IsHandEdited(managedTextWithOldValue)); + Assert.Equal("2048MB", Assert.Single(ActiveValues(managedTextWithOldValue, "maintenance_work_mem"))); await File.WriteAllTextAsync(managedConfPath, managedTextWithOldValue, timeout.Token); second = new DarlingManagedPostgres(config, NullLogger.Instance, runtimeRoot); @@ -1378,11 +1379,8 @@ fallback path would have to handle instead. */ the cap: the server must run on exactly the rendered value, and that value must be one PostgreSQL 17 accepts. */ var managedTextAfter = await File.ReadAllTextAsync(managedConfPath, timeout.Token); - Assert.Equal(1, CountOccurrences(managedTextAfter, "maintenance_work_mem = ")); - Assert.DoesNotContain("maintenance_work_mem = '2048MB'", managedTextAfter, StringComparison.Ordinal); - var renderedValue = System.Text.RegularExpressions.Regex.Match( - managedTextAfter, "maintenance_work_mem = '([^']*)'").Groups[1].Value; - Assert.False(string.IsNullOrEmpty(renderedValue), "expected the re-rendered darling-managed.conf to carry a maintenance_work_mem value"); + var renderedValue = Assert.Single(ActiveValues(managedTextAfter, "maintenance_work_mem")); + Assert.NotEqual("2048MB", renderedValue); var (live, expected) = await ReadSettingAndLiteralBytesAsync( connectionString, "maintenance_work_mem", renderedValue, timeout.Token); @@ -1390,9 +1388,11 @@ PostgreSQL 17 accepts. */ Assert.True(live <= DarlingManagedPostgres.MaintenanceWorkMemCapMb * 1024L * 1024L, $"expected the rendered maintenance_work_mem ({renderedValue}) to be at or under the {DarlingManagedPostgres.MaintenanceWorkMemCapMb} MB cap on PostgreSQL 17"); + /* initdb's postgresql.conf carries a commented sample line (#maintenance_work_mem = 64MB), so these + read ACTIVE assignments the way PostgreSQL does, never a raw substring. */ var postgresqlConfText = await File.ReadAllTextAsync(confPath, timeout.Token); - Assert.Contains(ManagedConfFile.IncludeLine, postgresqlConfText, StringComparison.Ordinal); - Assert.DoesNotContain("maintenance_work_mem", postgresqlConfText, StringComparison.Ordinal); + Assert.True(ManagedConfFile.HasManagedInclude(postgresqlConfText)); + Assert.Empty(ActiveValues(postgresqlConfText, "maintenance_work_mem")); } finally { @@ -1470,7 +1470,7 @@ public async Task Postgres17Store_OperatorLineBelowIncludeOutOfRange_RefusesToSt await first.EnsureRunningAsync(timeout.Token); await first.StopIfStartedByThisProcessAsync(); Assert.Equal(17, DarlingStoreUpgrade.TryReadDataDirectoryMajor(dataDirectory)); - Assert.Contains(ManagedConfFile.IncludeLine, await File.ReadAllTextAsync(confPath, timeout.Token), StringComparison.Ordinal); + Assert.True(ManagedConfFile.HasManagedInclude(await File.ReadAllTextAsync(confPath, timeout.Token))); /* The first start above succeeded, so it saved a last-good managed-conf copy (design step 2 -- see the summary above): this data directory does have one to fall back to. */ @@ -1480,6 +1480,7 @@ public async Task Postgres17Store_OperatorLineBelowIncludeOutOfRange_RefusesToSt /* An operator's own line below the include -- never something this product wrote (see the summary above). PostgreSQL 17 rejects it outright. */ await File.AppendAllTextAsync(confPath, "\n# an operator's own line\nmaintenance_work_mem = 2048MB\n", timeout.Token); + Assert.Equal("2048MB", Assert.Single(ActiveValues(await File.ReadAllTextAsync(confPath, timeout.Token), "maintenance_work_mem"))); second = new DarlingManagedPostgres(config, NullLogger.Instance, runtimeRoot); var thrown = await Assert.ThrowsAsync( @@ -3667,6 +3668,48 @@ await reader.ReadAsync(cancellationToken), return (reader.GetInt64(0), reader.GetInt64(1)); } + /// + /// The values of every ACTIVE assignment of in postgresql.conf-format text, read + /// with the product's own parser (), so a commented line + /// such as initdb's #maintenance_work_mem = 64MB sample never counts. + /// + private static List ActiveValues(string confText, string setting) + { + var values = new List(); + foreach (var (_, name, value) in DarlingManagedPostgres.ParseConfText(confText)) + { + if (name.Equals(setting, StringComparison.OrdinalIgnoreCase)) + { + values.Add(value); + } + } + + return values; + } + + /// + /// skips initdb's commented sample lines. The lines below are copied from + /// PostgreSQL 17's own postgresql.conf.sample, which is what initdb writes, and a raw substring check + /// on the key name matches them (the failure the gated cap facts above hit on a real PostgreSQL 17 conf). + /// + [Fact] + public void ActiveValues_SkipsInitdbsCommentedSampleLines_AndCountsAnOperatorLine() + { + const string initdbSample = + "#work_mem = 4MB\t\t\t\t# min 64kB\n" + + "#hash_mem_multiplier = 2.0\t\t# 1-1000.0 multiplier on hash table work_mem\n" + + "#maintenance_work_mem = 64MB\t\t# min 64kB\n" + + "#autovacuum_work_mem = -1\t\t# min 64kB, or -1 to use maintenance_work_mem\n" + + "include 'darling-managed.conf'\n"; + + Assert.Contains("maintenance_work_mem", initdbSample, StringComparison.Ordinal); + Assert.Empty(ActiveValues(initdbSample, "maintenance_work_mem")); + Assert.True(ManagedConfFile.HasManagedInclude(initdbSample)); + + var withOperatorLine = initdbSample + "\n# an operator's own line\nmaintenance_work_mem = 2048MB\n"; + Assert.Equal("2048MB", Assert.Single(ActiveValues(withOperatorLine, "maintenance_work_mem"))); + } + private static int CountOccurrences(string text, string value) { var count = 0;