Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions .github/scripts/assert-bake-consumption.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,19 @@ echo "$LABEL: seed postcondition — adopted=$adopted declared=$expected"
[ "$expected" -gt 0 ] \
|| fail "$LABEL: the gate adopted $adopted of 0 baked assembly(ies) — the bake declared nothing this run installed, so the gate judged NONE of the bytes that ship. Bake and gate must be staged from the same tree."

# 🚨 `>=`, NOT `=`, and that is the tester's OWN contract (BakeSeed.Shortfall: `if (Adopted >=
# expected.Count) return null;`) rather than a loosened one. `adopted` counts adoption EVENTS over
# the run while `declared` counts distinct baked types, and the gate installs every package TWICE
# (the idempotence pin re-installs the unchanged snapshot), so the second install adopts the same
# assemblies again. Measured on samples/Graph/Data: `adopted 32 of 28`. An equality test here would
# be red on every healthy run — and a gate that cries wolf is a gate that gets switched off.
[ "$adopted" -ge "$expected" ] \
# 🚨 `=`, and it can be `=` since #2697. This was `>=` because the tester reported adoption EVENTS:
# the gate installs every package TWICE (the idempotence pin re-installs the unchanged snapshot),
# so the second install adopted the same assemblies again and a healthy run measured `adopted 32 of
# 28` on samples/Graph/Data. An equality test would have been red on every healthy run then — and a
# gate that cries wolf gets switched off.
#
# The tester now reports BakeSeedConsumer.AdoptedPaths: the size of the set of distinct paths that
# were declared AND requested AND backed — the exact complement of the DECLINED list in the same
# verdict. It cannot exceed `expected` however many times the seeder acted, so `>=` had become a
# strictly weaker way of writing `=`, and the reason for the weakness was a bug rather than a
# property of the run. Equality now says what this check actually means: every baked assembly this
# run installed was judged from the bake's own bytes.
[ "$adopted" -eq "$expected" ] \
Comment on lines +70 to +82
|| fail "$LABEL: the gate adopted only $adopted of $expected baked assembly(ies) — the rest were DECLINED and compiled locally. PrebuiltAssemblySeeder logs the per-assembly reason (framework identity, or the per-type dependency record)."

[ "$fallback_hit" = "0" ] || exit 1
Expand Down
45 changes: 39 additions & 6 deletions test/MeshWeaver.PluginTester.Test/BakeShortfallNamesTheTypeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public void Names_the_declined_type_rather_than_only_counting_it()
declared,
requested: declared,
covered: Set("Crm/Board", "Store/Plugin"),
adopted: 2,
directory: "/bake");

Assert.NotNull(verdict);
Expand All @@ -46,7 +45,7 @@ public void Silent_when_everything_declared_and_requested_was_backed()
{
var declared = Set("Crm/Board", "Crm/Client");
Assert.Null(BakeSeedConsumer.DescribeShortfall(
declared, requested: declared, covered: declared, adopted: 2, directory: "/bake"));
declared, requested: declared, covered: declared, directory: "/bake"));
}

[Fact]
Expand All @@ -57,7 +56,6 @@ public void A_type_the_run_never_installed_is_not_a_shortfall()
declared: Set("Crm/Board", "Other/Type"),
requested: Set("Crm/Board"),
covered: Set("Crm/Board"),
adopted: 1,
directory: "/bake");
Assert.Null(verdict);
}
Expand All @@ -69,7 +67,6 @@ public void No_overlap_at_all_still_reports_the_staging_error()
declared: Set("Crm/Board"),
requested: Set("Something/Else"),
covered: Set(),
adopted: 0,
directory: "/bake");
Assert.NotNull(verdict);
Assert.Contains("NONE of which this run installed", verdict);
Expand All @@ -82,12 +79,48 @@ public void Counting_alone_could_not_distinguish_these_two_runs()
// point of the change, and precisely what a count-based verdict conflated.
var declared = Set("A/One", "B/Two", "C/Three");
var missingB = BakeSeedConsumer.DescribeShortfall(
declared, declared, Set("A/One", "C/Three"), adopted: 2, directory: "/bake");
declared, declared, Set("A/One", "C/Three"), directory: "/bake");
var missingC = BakeSeedConsumer.DescribeShortfall(
declared, declared, Set("A/One", "B/Two"), adopted: 2, directory: "/bake");
declared, declared, Set("A/One", "B/Two"), directory: "/bake");

Assert.Contains("DECLINED: B/Two", missingB);
Assert.Contains("DECLINED: C/Three", missingC);
Assert.NotEqual(missingB, missingC);
}

[Fact]
public void A_path_seeded_under_two_packages_is_counted_ONCE()
{
// #2697: `Adopted` summed the seeder's per-call counts, so a path covered under two
// packages counted twice and the verdict could read "adopted 92 of 90" — a number larger
// than the total it is a fraction of. The count is now derived from the same sets the
// DECLINED list comes from, so it cannot exceed the denominator however many times the
// seeder acted.
var declared = Set("A/One", "B/Two");
var covered = Set("A/One", "B/Two");

// Whatever the seeder DID (two packages both seeding both paths = 4 events), the verdict
// is silent because every expected path is covered...
Assert.Null(BakeSeedConsumer.DescribeShortfall(
declared, requested: declared, covered: covered, directory: "/bake"));

// ...and where there IS a shortfall, adopted + declined == expected, by construction.
var verdict = BakeSeedConsumer.DescribeShortfall(
declared, requested: declared, covered: Set("A/One"), directory: "/bake");
Assert.Contains("adopted 1 of 2", verdict);
Assert.Contains("1 were DECLINED", verdict);
}

[Fact]
public void AdoptedAmong_counts_the_intersection_not_the_events()
{
// The pure seam the property rests on: only paths that are declared AND requested AND
// covered count, each exactly once.
Assert.Equal(2, BakeSeedConsumer.AdoptedAmong(
declared: Set("A/One", "B/Two", "C/Three"),
requested: Set("A/One", "B/Two"),
covered: Set("A/One", "B/Two", "C/Three")));
Assert.Equal(0, BakeSeedConsumer.AdoptedAmong(
declared: Set("A/One"), requested: Set("B/Two"), covered: Set("A/One", "B/Two")));
}
}
39 changes: 34 additions & 5 deletions tools/MeshWeaver.PluginTester/BakeSeed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,34 @@ public ImmutableSortedSet<string> Requested
get { lock (gate) return requested; }
}

/// <summary>Assemblies adopted from the bake across the whole run.</summary>
/// <summary>
/// Adoption EVENTS across the whole run — the sum of the per-call counts the seeder returned.
///
/// <para>🚨 This is NOT the number of assemblies adopted, and it must never be reported as one.
/// A path covered under two packages is seeded twice and counted twice, which is how a gate
/// reported the impossible <c>adopted 92 of 90</c> (#2697). Use <see cref="AdoptedPaths"/> for
/// any verdict or log line; this stays because "how many times did the seeder act" is a
/// different, occasionally useful fact.</para>
/// </summary>
public int Adopted => Volatile.Read(ref adopted);

/// <summary>
/// The number of DISTINCT baked assemblies this run adopted, counted over the same set the
/// DECLINED list is the complement of — so <c>AdoptedPaths + declined == expected</c> holds by
/// construction rather than by luck.
/// </summary>
public int AdoptedPaths => AdoptedAmong(seed.DeclaredTypePaths, Requested, Covered);

/// <summary>
/// The adopted set: everything the bake declared AND the install requested AND the seeder
/// backed. Pure, so the accounting can be pinned without standing up a mesh.
/// </summary>
public static int AdoptedAmong(
ImmutableSortedSet<string> declared,
ImmutableSortedSet<string> requested,
ImmutableSortedSet<string> covered)
=> declared.Intersect(requested).Intersect(covered).Count;

/// <summary>Every NodeType path the seeder BACKED from this bake — adopted now or already
/// current. The witness behind <see cref="Shortfall"/>'s named verdict.</summary>
public ImmutableSortedSet<string> Covered
Expand Down Expand Up @@ -214,7 +239,7 @@ public IObservable<int> SeedForTypes(IReadOnlyCollection<string> typePaths)
/// the same shape as a skipped CI job wearing a passing tick.</para>
/// </summary>
public string? Shortfall() =>
DescribeShortfall(seed.DeclaredTypePaths, Requested, Covered, Adopted, seed.Directory);
DescribeShortfall(seed.DeclaredTypePaths, Requested, Covered, seed.Directory);

/// <summary>
/// <see cref="Shortfall"/>'s verdict as a PURE function of the four facts it rests on, so the
Expand All @@ -224,13 +249,16 @@ public IObservable<int> SeedForTypes(IReadOnlyCollection<string> typePaths)
/// <param name="declared">Every path the bake carries bytes for.</param>
/// <param name="requested">Every path the install asked the consumer about.</param>
/// <param name="covered">Every path the seeder actually backed.</param>
/// <param name="adopted">The seeder's own count, reported as-is.</param>
/// <param name="directory">The bake directory, for the no-overlap message.</param>
/// <remarks>🚨 There is deliberately no <c>adopted</c> parameter. It used to take the seeder's
/// running EVENT count and report it as-is, so a path covered under two packages counted twice
/// and the verdict could read <c>adopted 92 of 90</c> — an impossible sentence in the one line
/// meant to be trusted (#2697). The adopted number is now DERIVED from the same sets the
/// DECLINED list comes from, so the arithmetic cannot disagree with the naming.</remarks>
public static string? DescribeShortfall(
ImmutableSortedSet<string> declared,
ImmutableSortedSet<string> requested,
ImmutableSortedSet<string> covered,
int adopted,
string directory)
{
var expected = declared.Intersect(requested);
Expand All @@ -253,7 +281,8 @@ public IObservable<int> SeedForTypes(IReadOnlyCollection<string> typePaths)
// WARNING — the pointer led to lines the gate never writes, which is why a 1-of-87
// shortfall was undiagnosable from a CI log (MeshWeaver.Crm main, red from 2026-08-28
// 21:54 for ~12 h with every per-type verdict green). A verdict carries its own evidence.
return $"the gate adopted {adopted} of {expected.Count} baked assembly(ies) for the types "
return $"the gate adopted {expected.Intersect(covered).Count} of {expected.Count} baked "
+ "assembly(ies) for the types "
+ $"it installed — {missing.Count} were DECLINED and compiled locally, so the gate did "
+ "not judge the bytes the bake shipped for them. DECLINED: "
+ $"{string.Join(", ", missing.Take(20))}"
Expand Down
2 changes: 1 addition & 1 deletion tools/MeshWeaver.PluginTester/PluginGateRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ private static GateReport WithSeedVerdict(
return report;
var expected = consumer.Seed.DeclaredTypePaths.Intersect(consumer.Requested).Count;
options.Output.WriteLine(
$"seed: adopted {consumer.Adopted} of {expected} baked assembly(ies) for the "
$"seed: adopted {consumer.AdoptedPaths} of {expected} baked assembly(ies) for the "
+ $"{consumer.Requested.Count} installed NodeType(s) "
+ $"(bake: {consumer.Seed.Describe()})");
if (consumer.Shortfall() is not { } shortfall)
Expand Down
Loading