fix(chargen): Campaign CC gate round 1 closeout — Group 3: round review fixes (F4-F11, F14, F16)

The remaining code-bearing findings from the round review, F4-F16 minus
the doc-only items (batched separately):

- F4: three client-wide UiButton corpus sweeps (LabelBox path — exactly
  the 4 Town buttons, confined to chargen; conflicting custom-selection-
  pair + standard Normal/Highlight media — zero found, no gate
  tightening needed; per-state label-color map — 209 matches beyond
  chargen, confirming AP-222's mechanism has always been broadly active
  since it shipped generically in DatWidgetFactory).
- F5/F6: LayoutImporter's Batch C un-consumed-children carve-out now
  honors a child's own AuthoredInvisible flag (a narrow honor scoped to
  exactly that carve-out, not the general #408 client-wide one) — the
  chat transcript's new-text indicator (0x1000048C) was building as a
  visible phantom element retail never shows; verified both directions
  against the gold-frame pieces, which do not author Invisible.
- F7: BoundedProcessOutputCapture.AppendLine combines the line text and
  its trailing newline into one buffer and one file open/write/close
  instead of two.
- F9: corrected a stale comment in RuntimeSettingsTargets — #407 split
  DisplayModeCatalog's Resolutions/WindowedResolutions in two, so the
  fullscreen validator's own narrower list is now DELIBERATELY different
  from the Config dropdown's fuller offering, not the "must match" bug
  the comment described.
- F10: documented (not changed) why the LabelBox path's default 3px
  inset and the face-relative +4px gap in DatWidgetFactory.BuildButton
  are deliberately different numbers — neither carries a retail
  citation, and moving either to match the other would be an unfounded
  guess on a button that currently works correctly.
- F11: Heritage/Profession/Summary/Town description pages now compose
  DatRichText.Compose's result ONCE inside their already revision-gated
  Refresh, caching the built line list instead of re-wrapping on every
  draw call.
- F14: documented (not changed) why PrivateEntityViewportRenderer's
  _animatedIds set carrying a reserved-but-never-drawn backdrop id is
  harmless — BuildDrawEntities already excludes a null/empty backdrop
  from the actual draw list, so the id is never looked up.
- F16: the Summary preview now uses its own render-id pair
  (SummaryPreviewRenderId/SummaryPreviewBackdropRenderId, 0xDA11D035/
  0xDA11D036) instead of sharing the Appearance page's
  (0xDA11D032/0xDA11D034) — confirmed by tracing
  FixedEntityTextureOwnerLease through TextureCache to
  CompositeTextureArrayCache's shared owner tracker that both pages'
  previews share ONE process-wide TextureCache, so sharing render ids
  was a real cross-page texture-release collision (either page's own
  re-dress or disposal could release the OTHER page's still-active
  textures), not a theoretical one.

F3's own register bookkeeping (AP-229 addendum) and F12's register/AD
header-count corrections land in the docs-only commit alongside F15.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-16 15:34:07 +02:00
parent 0fed5fdd91
commit bd359d5181
17 changed files with 636 additions and 37 deletions

View file

@ -66,6 +66,57 @@ public sealed class ChargenPreviewEntityBuilderTests
_out.WriteLine($"setup=0x{appearance.SetupId:X8} meshRefs={entity.MeshRefs.Count} subPalettes={entity.PaletteOverride.SubPalettes.Count}");
}
/// <summary>
/// F16 (Campaign CC gate round 1 closeout): the Appearance and Summary
/// pages must stamp DIFFERENT <see cref="AcDream.Core.World.WorldEntity.Id"/>
/// values on their preview entities — both feed the SAME shared
/// <c>TextureCache</c> owner-tracking key
/// (<c>ChargenPreviewEntityBuilder.SummaryPreviewRenderId</c>'s own doc
/// has the full collision trace). Pins BOTH halves: the constants
/// themselves are distinct, AND the explicit <c>renderId</c> parameter
/// actually reaches the built entity (not silently ignored).
/// </summary>
[Fact]
public void TryBuild_ExplicitRenderId_StampsThatIdOnTheEntity_DistinctFromTheAppearanceDefault()
{
Assert.NotEqual(
ChargenPreviewEntityBuilder.PreviewRenderId,
ChargenPreviewEntityBuilder.SummaryPreviewRenderId);
Assert.NotEqual(
ChargenPreviewEntityBuilder.PreviewBackdropRenderId,
ChargenPreviewEntityBuilder.SummaryPreviewBackdropRenderId);
string? datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
using var adapter = new DatCollectionAdapter(dats);
ChargenOptions options = ChargenTableReader.Load(adapter);
Assert.True(options.TryGetHeritage(1u, out ChargenHeritageOptions? aluvian)); // Aluvian.
Assert.True(aluvian!.GendersByKey.TryGetValue(1, out ChargenGenderOptions? male));
var catalog = new ChargenAppearanceCatalog(adapter);
ChargenAppearanceSelection selection = ChargenAppearanceSelection.Default with
{
HairStyle = male!.HairStyles.Count > 0 ? 0u : ChargenAppearanceSelection.Unset,
SkinShade = 0.5,
};
bool composed = ChargenAppearanceFactory.TryCompose(
options, 1u, 1, selection, catalog, catalog, out ChargenAppearanceResult appearance);
Assert.True(composed);
var animations = new RetailAnimationLoader(adapter);
var entity = ChargenPreviewEntityBuilder.TryBuild(
adapter, animations, appearance, heritageId: 1u, Quaternion.Identity, new object(),
renderId: ChargenPreviewEntityBuilder.SummaryPreviewRenderId);
Assert.NotNull(entity);
Assert.Equal(ChargenPreviewEntityBuilder.SummaryPreviewRenderId, entity!.Id);
Assert.NotEqual(ChargenPreviewEntityBuilder.PreviewRenderId, entity.Id);
}
[Fact]
public void TryBuild_UnknownSetupId_ReturnsNull()
{
@ -317,6 +368,36 @@ public sealed class ChargenPreviewEntityBuilderTests
_out.WriteLine($"backdropSetup=0x{aluvian.EnvironmentSetupId:X8} meshRefs={entity.MeshRefs.Count}");
}
/// <summary>F16 (Campaign CC gate round 1 closeout): the backdrop's own
/// explicit <c>renderId</c> parameter reaches the built entity, the
/// same shape as <see cref="TryBuild_ExplicitRenderId_StampsThatIdOnTheEntity_DistinctFromTheAppearanceDefault"/>
/// pins for the main preview entity.</summary>
[Fact]
public void TryBuildBackdrop_ExplicitRenderId_StampsThatIdOnTheEntity()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
using var adapter = new DatCollectionAdapter(dats);
ChargenOptions options = ChargenTableReader.Load(adapter);
Assert.True(options.TryGetHeritage(1u, out ChargenHeritageOptions? aluvian)); // Aluvian.
if (aluvian!.EnvironmentSetupId == 0u)
{
_out.WriteLine("SKIP: installed dat's Aluvian heritage authors no EnvironmentSetupId.");
return;
}
var entity = ChargenPreviewEntityBuilder.TryBuildBackdrop(
adapter, aluvian.EnvironmentSetupId, new object(),
renderId: ChargenPreviewEntityBuilder.SummaryPreviewBackdropRenderId);
Assert.NotNull(entity);
Assert.Equal(ChargenPreviewEntityBuilder.SummaryPreviewBackdropRenderId, entity!.Id);
Assert.NotEqual(ChargenPreviewEntityBuilder.PreviewBackdropRenderId, entity.Id);
}
/// <summary>Retail's own gate at 0x004eed29 (<c>if (eax_32 != INVALID_DID.id)</c>)
/// skips creating a backdrop object entirely when the heritage authors no
/// environment Setup — id 0/unset must return null, not an empty entity.</summary>

View file

@ -86,23 +86,32 @@ public sealed class LayoutImporterMediaBearingChildSweepTests
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
// MAIN GAME UI (0x21000005/0x1000059A): the same eight gold-frame
// pieces the chargen boxes carry.
// pieces the chargen boxes carry. F5/F6 closeout: NONE of these
// author Invisible, so all eight must build VISIBLE — the "other
// direction" the reviewer named, pinned here alongside the
// enumeration sweep's own DoesNotContain assertions.
AssertChildrenBuild(
dats, layoutId: 0x21000005u, elementId: 0x1000059Au,
expectedChildIds:
[
0x100002DEu, 0x100002DFu, 0x100002E0u, 0x100002E1u,
0x100000E8u, 0x100002E2u, 0x100002E3u, 0x100000EAu,
]);
],
expectedInvisible: []);
// CHAT INPUT (0x2100006F/0x10000011): a single media-bearing child.
// Chat transcript (0x2100006F/0x10000011): a single media-bearing
// child. F5/F6 closeout: 0x1000048C (the new-text indicator)
// authors Invisible=true — must build HIDDEN, not as a phantom
// visible element.
AssertChildrenBuild(
dats, layoutId: 0x2100006Fu, elementId: 0x10000011u,
expectedChildIds: [0x1000048Cu]);
expectedChildIds: [0x1000048Cu],
expectedInvisible: [0x1000048Cu]);
}
private static void AssertChildrenBuild(
IDatReaderWriter dats, uint layoutId, uint elementId, uint[] expectedChildIds)
IDatReaderWriter dats, uint layoutId, uint elementId, uint[] expectedChildIds,
uint[] expectedInvisible)
{
ElementInfo? tree = LayoutImporter.ImportInfos(dats, layoutId);
Assert.NotNull(tree);
@ -114,7 +123,10 @@ public sealed class LayoutImporterMediaBearingChildSweepTests
Assert.IsType<UiText>(built);
foreach (uint childId in expectedChildIds)
{
Assert.NotNull(UiElement.FindDescendant(built, childId));
UiElement? child = UiElement.FindDescendant(built, childId);
Assert.NotNull(child);
bool shouldBeInvisible = expectedInvisible.Contains(childId);
Assert.Equal(!shouldBeInvisible, child!.Visible);
}
}
@ -129,6 +141,78 @@ public sealed class LayoutImporterMediaBearingChildSweepTests
return null;
}
private readonly record struct InvisibleChildFinding(uint LayoutId, uint ParentElementId, uint ChildElementId);
/// <summary>
/// Campaign CC gate round 1 closeout, F5/F6: of the media-bearing
/// children the Batch C carve-out now builds instead of dropping
/// (<see cref="MediaBearingChildSweep_EnumeratesEveryAffectedType12Element"/>'s
/// own set), which ones author dat property <c>0x3B</c> (Invisible)
/// THEMSELVES — retail would never show them
/// (<c>UIElement::OnSetAttribute @0x00462d80</c> case 8), so building
/// them unconditionally as a visible widget is a regression the
/// carve-out's own commit didn't check for. Confirms the chargen
/// gold-frame pieces are NOT among them (the other direction the
/// reviewer named — Batch A's chargen-scoped hide walk must not eat
/// them either way, but this proves the DATA itself never marks them
/// invisible, independent of which honor mechanism runs).
/// </summary>
[InstalledDatFact]
public void MediaBearingChildSweep_EnumeratesWhichAffectedChildrenAuthorInvisible()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
var invisibleFindings = new List<InvisibleChildFinding>();
foreach (uint layoutId in dats.GetAllIdsOfType<LayoutDesc>().OrderBy(x => x))
{
ElementInfo? tree = LayoutImporter.ImportInfos(dats, layoutId);
if (tree is null) continue;
WalkForInvisibleMediaBearingChildren(layoutId, tree, invisibleFindings);
}
Console.WriteLine($"[SWEEP-INV] {invisibleFindings.Count} media-bearing children of the "
+ "Batch C carve-out author Invisible=true themselves.");
foreach (InvisibleChildFinding f in invisibleFindings)
{
Console.WriteLine($"[SWEEP-INV] layout=0x{f.LayoutId:X8} parent=0x{f.ParentElementId:X8} "
+ $"child=0x{f.ChildElementId:X8}");
}
// The chargen gold-frame pieces (GF-12) must NOT author Invisible —
// otherwise a narrow per-child honor would eat them, undoing that
// fix. Checked directly against the data, independent of whichever
// honor mechanism runs.
uint[] goldFramePieceIds =
[
0x100002DEu, 0x100002DFu, 0x100002E0u, 0x100002E1u,
0x100000E8u, 0x100002E2u, 0x100002E3u, 0x100000EAu,
];
foreach (uint pieceId in goldFramePieceIds)
{
Assert.DoesNotContain(invisibleFindings, f => f.ChildElementId == pieceId);
}
}
private static void WalkForInvisibleMediaBearingChildren(
uint layoutId, ElementInfo node, List<InvisibleChildFinding> findings)
{
if (node.Type == 12u)
{
bool passToChildren = node.States.Values.Any(static s => s.PassToChildren);
if (!passToChildren)
{
foreach (ElementInfo child in node.Children)
{
if (child.StateMedia.Count > 0 && child.Invisible)
findings.Add(new InvisibleChildFinding(layoutId, node.Id, child.Id));
}
}
}
foreach (ElementInfo child in node.Children)
WalkForInvisibleMediaBearingChildren(layoutId, child, findings);
}
private static void Walk(uint layoutId, ElementInfo node, List<Finding> findings)
{
if (node.Type == 12u)

View file

@ -0,0 +1,214 @@
using System.IO;
using System.Linq;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Content;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
namespace AcDream.App.Tests.UI.Layout;
/// <summary>
/// Campaign CC gate round 1 closeout, F4: three client-wide blast-radius
/// sweeps over EVERY installed <c>LayoutDesc</c>, walking every Type-1
/// (<c>UIElement_Button</c>) element and matching the SAME structural
/// predicates <see cref="DatWidgetFactory.BuildButton"/> uses internally
/// (predicates re-derived here rather than reflected, since the source
/// methods are <c>private</c> — kept in sync by citing the exact source
/// line ranges in each sweep's own doc). Same style as
/// <see cref="LayoutImporterMediaBearingChildSweepTests"/>: logs the full
/// enumeration for the commit message, pins landmark counts rather than a
/// brittle exact global total.
/// </summary>
public sealed class UiButtonCorpusSweepTests
{
private static string DatDirectory =>
System.Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? Path.Combine(
System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile),
"Documents",
"Asheron's Call");
private readonly record struct ButtonFinding(uint LayoutId, uint ElementId);
/// <summary>
/// Sweep (a): which buttons take the GF-11c <c>LabelBox</c> path —
/// <c>info.StateMedia.Count==0</c> (no media on the button itself) with
/// EXACTLY one stateful face child, plus a DISTINCT lifted Type-12
/// caption child (not the button's own P0x17) — see
/// <c>DatWidgetFactory.BuildButton</c>:889-914 for the exact shape this
/// mirrors.
/// </summary>
[InstalledDatFact]
public void LabelBoxPath_EnumeratesEveryMatchingButton()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
var findings = new List<ButtonFinding>();
foreach (uint layoutId in dats.GetAllIdsOfType<LayoutDesc>().OrderBy(x => x))
{
ElementInfo? tree = LayoutImporter.ImportInfos(dats, layoutId);
if (tree is null) continue;
WalkButtons(layoutId, tree, findings, MatchesLabelBoxShape);
}
Console.WriteLine($"[SWEEP-A] {findings.Count} buttons take the LabelBox path across "
+ $"{findings.Select(f => f.LayoutId).Distinct().Count()} layouts.");
foreach (ButtonFinding f in findings.OrderBy(f => f.LayoutId).ThenBy(f => f.ElementId))
Console.WriteLine($"[SWEEP-A] layout=0x{f.LayoutId:X8} element=0x{f.ElementId:X8}");
// Landmark this campaign already fixed and gated (GF-11c, the Town
// page's per-marker name label) must be in the set — proves the
// sweep's predicate is right, not just non-empty.
Assert.Contains(findings, f => IsTownButton(f.ElementId));
}
/// <summary>
/// Sweep (b): any button authoring BOTH the custom Unselected/Selected
/// radio-pair (<c>UiButton</c>'s <c>_hasCustomSelectionPair</c> bypass,
/// GF-1/GF-8) AND standard Normal/Highlight media — the custom-pair
/// bypass would eat the standard state machine for such a button
/// (<c>UiButton.UpdateVisualState</c>'s <c>if (_hasCustomSelectionPair)</c>
/// branch runs UNCONDITIONALLY when the pair is present, never falling
/// through to the standard <c>_availableStates</c> branch). None found
/// in the installed corpus at the ELEMENT's own media level (this sweep
/// does not additionally check face-SEGMENT media — see this method's
/// own note).
/// </summary>
[InstalledDatFact]
public void CustomSelectionPair_NeverCoexistsWithStandardNormalHighlightMedia()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
var findings = new List<ButtonFinding>();
foreach (uint layoutId in dats.GetAllIdsOfType<LayoutDesc>().OrderBy(x => x))
{
ElementInfo? tree = LayoutImporter.ImportInfos(dats, layoutId);
if (tree is null) continue;
WalkButtons(layoutId, tree, findings, MatchesConflictingPairShape);
}
Console.WriteLine($"[SWEEP-B] {findings.Count} buttons author BOTH the custom "
+ "Unselected/Selected pair AND standard Normal/Highlight media.");
foreach (ButtonFinding f in findings)
Console.WriteLine($"[SWEEP-B] layout=0x{f.LayoutId:X8} element=0x{f.ElementId:X8}");
// No conflict exists in the installed corpus today — the
// `_hasCustomSelectionPair` bypass in `UiButton.UpdateVisualState`
// is safe as-is (unconditional-when-present) without needing a
// tighter gate. If a future DAT drop introduces one, this test
// fails here rather than silently regressing that button's
// Highlight/rollover feedback.
Assert.Empty(findings);
}
/// <summary>
/// Sweep (c): any button with a genuine per-state label-color/outline
/// map (AP-222's mechanism, <c>ElementReader.BuildPerStateColorMap</c>/
/// <c>BuildPerStateBoolMap</c> against dat properties <c>0x1B</c>/
/// <c>0x21</c> — non-null only when the authored dat carries MORE THAN
/// ONE distinct value across states) beyond the chargen Appearance
/// spins and Town buttons this campaign already ported and gated.
/// </summary>
[InstalledDatFact]
public void PerStateLabelColorMap_EnumeratesEveryButtonBeyondChargen()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
var findings = new List<ButtonFinding>();
foreach (uint layoutId in dats.GetAllIdsOfType<LayoutDesc>().OrderBy(x => x))
{
ElementInfo? tree = LayoutImporter.ImportInfos(dats, layoutId);
if (tree is null) continue;
WalkButtons(layoutId, tree, findings, MatchesPerStateLabelStyleShape);
}
Console.WriteLine($"[SWEEP-C] {findings.Count} buttons carry a genuine per-state "
+ "label color/outline map.");
foreach (ButtonFinding f in findings.OrderBy(f => f.LayoutId).ThenBy(f => f.ElementId))
Console.WriteLine($"[SWEEP-C] layout=0x{f.LayoutId:X8} element=0x{f.ElementId:X8}");
// Landmarks this campaign already ported: the nine Appearance spins
// (Hair/Eyes/Skin/Headgear/Shirt/Trousers/Footwear/Nose/Mouth, all
// sharing one Highlight-gold-brightening state pair) and the four
// Town buttons (Normal-gold -> Selected-white caption swap).
Assert.Contains(findings, f =>
f.ElementId == CharacterCreationAppearancePage.HairSpinId);
Assert.Contains(findings, f => IsTownButton(f.ElementId));
}
/// <summary>Town page's four starting-area button ids
/// (<c>CharacterCreationTownPage</c>'s own private
/// <c>StartAreaByButtonId</c> keys — no public constants exist there,
/// so the literals are duplicated here).</summary>
private static bool IsTownButton(uint elementId) => elementId is
0x1000040Bu or 0x1000040Du or 0x1000040Eu or 0x1000040Fu;
// ── Shared predicates (re-derived from DatWidgetFactory.BuildButton) ──
private static bool MatchesLabelBoxShape(ElementInfo info)
{
if (info.StateMedia.Count != 0)
return false;
ElementInfo[] faces = FindStatefulFaceChildren(info);
if (faces.Length != 1)
return false;
// A DISTINCT lifted Type-12 caption child (not the button's own
// P0x17) — DatWidgetFactory.BuildButton's own "label is null on the
// button itself, found on a Type-12 child instead" fallback.
bool ownCaption = HasStringInfoProperty(info);
if (ownCaption)
return false;
return info.Children.Any(child => child.Type == 12u && HasStringInfoProperty(child));
}
private static bool MatchesConflictingPairShape(ElementInfo info)
{
bool hasCustomPair = info.StateMedia.ContainsKey("Unselected") && info.StateMedia.ContainsKey("Selected");
bool hasStandardPair = info.StateMedia.ContainsKey("Normal") || info.StateMedia.ContainsKey("Highlight");
return hasCustomPair && hasStandardPair;
}
private static bool MatchesPerStateLabelStyleShape(ElementInfo info)
{
// Mirror BuildButton's labelInfo resolution: the button's own P0x17
// if present, else the first Type-12 child with a resolvable one.
ElementInfo labelInfo = HasStringInfoProperty(info)
? info
: info.Children.FirstOrDefault(child => child.Type == 12u && HasStringInfoProperty(child)) ?? info;
return ElementReader.BuildPerStateColorMap(labelInfo, 0x1Bu) is not null
|| ElementReader.BuildPerStateBoolMap(labelInfo, 0x21u) is not null;
}
private static bool HasStringInfoProperty(ElementInfo info) =>
info.TryGetEffectiveProperty(0x17u, out UiPropertyValue property)
&& property.Kind == UiPropertyKind.StringInfo;
/// <summary>Verbatim copy of <c>DatWidgetFactory.FindStatefulFaceChildren</c>
/// (private there) — a child whose media state names intersect the
/// PARENT's own declared state names.</summary>
private static ElementInfo[] FindStatefulFaceChildren(ElementInfo info) =>
[.. info.Children
.Where(child =>
child.StateMedia.Count != 0
&& child.StateMedia.Keys.Any(childState =>
info.States.Values.Any(parentState =>
string.Equals(parentState.Name, childState, StringComparison.Ordinal))))
.OrderBy(child => child.ReadOrder)];
private static void WalkButtons(
uint layoutId,
ElementInfo node,
List<ButtonFinding> findings,
Func<ElementInfo, bool> predicate)
{
if (node.Type == 1u && predicate(node))
findings.Add(new ButtonFinding(layoutId, node.Id));
foreach (ElementInfo child in node.Children)
WalkButtons(layoutId, child, findings, predicate);
}
}

View file

@ -81,6 +81,36 @@ public sealed class BoundedProcessOutputCaptureTests
}
}
/// <summary>F7 (Campaign CC gate round 1 closeout): <c>AppendLine</c>
/// now combines the text and its trailing newline into ONE buffer
/// before writing, instead of two separate file open/write/close
/// round-trips. Pins the boundary case that change touches most
/// directly — a line whose TEXT ALONE exactly exhausts the remaining
/// cap, so the newline byte must be dropped by the SAME truncation
/// decision as the text, not a second one.</summary>
[Fact]
public void ALineWhoseTextExactlyExhaustsTheCap_DropsOnlyTheTrailingNewline()
{
string path = TempPath();
try
{
// "0123456789" is exactly 10 bytes; maxBytes=10 leaves no room
// for the newline the combined buffer also carries.
using var capture = new BoundedProcessOutputCapture(path, maxBytes: 10);
capture.AppendLine("0123456789");
Assert.True(capture.IsDone);
string written = File.ReadAllText(path);
Assert.StartsWith("0123456789", written, StringComparison.Ordinal);
Assert.Contains("truncated at 10 bytes", written, StringComparison.Ordinal);
}
finally
{
TryDelete(path);
}
}
[Fact]
public void ALogSpammingChildCannotGrowTheFileUnboundedly()
{