Tests: CB-R1 enforced whole, backfill replay on a scratch catalog, envelope and operability read the catalog
ShippedCatalogRolesAreLive checks every design of every kind: each non-flat stat reads one declared role,
declared roles are read, sellers author each once, role-mapped designs are not all flat (the tool answers
which, via Program.HasRoleMap). RolesBackfillIsAuthored pins ratios and roles, with the Autocannon
Penetration, Mine Range and LRMM72 LockAngle asserts. RolesBackfillCommandTests resets the backfilled designs
on a scratch catalog, applies, and expects the shipped catalog back, then replays as a no-op.
EveryShippedProductHasAMaker. TheEnvelopeComesFromTheGear bounds each drive by the catalog's Large Drive
Thrust range; ShippedWeaponOperabilityTests reads the catalog's DefaultEntitySettings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 23:46:28 +02:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Reflection ;
using GameCult.Caching ;
using GameCult.Caching.MessagePack ;
using Xunit ;
// AetherDb's roles-backfill command (aetheria-release, cut roles-backfill) run on a scratch copy of the shipped catalog,
// with the designs it backfills put back to the state they shipped in before it: flat stats at the middle of their range,
// no roles, no seller quality, and the unnamed Quality terms of the stats that already varied. The shipped catalog is the
// oracle, since the command produced it.
public sealed class RolesBackfillCommandTests : IDisposable
{
2026-10-13 20:29:10 +02:00
private static readonly string [ ] Flattened = { "Autocannon" , "Long-Range Missile Rack" , "Short-Range Missile Rack" , "Mine Launcher" , "Large Drive" , "Small Drive" } ;
Tests: CB-R1 enforced whole, backfill replay on a scratch catalog, envelope and operability read the catalog
ShippedCatalogRolesAreLive checks every design of every kind: each non-flat stat reads one declared role,
declared roles are read, sellers author each once, role-mapped designs are not all flat (the tool answers
which, via Program.HasRoleMap). RolesBackfillIsAuthored pins ratios and roles, with the Autocannon
Penetration, Mine Range and LRMM72 LockAngle asserts. RolesBackfillCommandTests resets the backfilled designs
on a scratch catalog, applies, and expects the shipped catalog back, then replays as a no-op.
EveryShippedProductHasAMaker. TheEnvelopeComesFromTheGear bounds each drive by the catalog's Large Drive
Thrust range; ShippedWeaponOperabilityTests reads the catalog's DefaultEntitySettings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 23:46:28 +02:00
// Designs that shipped with non-flat stats whose Quality term named no role; the field is the stat to unname.
private static readonly ( string Design , string Field ) [ ] Unnamed =
{
2026-10-13 20:29:10 +02:00
( "Cluster Cannon" , "Count" ) , ( "Flak Gun" , "Count" ) , ( "Guided Missile Launcher" , "MinRange" ) , ( "Lightning Projector" , "DamageSpread" ) ,
( "Muon Fusion Reactor" , "Modifier" ) , ( "Muon Fusion Station Reactor" , "Modifier" ) ,
Tests: CB-R1 enforced whole, backfill replay on a scratch catalog, envelope and operability read the catalog
ShippedCatalogRolesAreLive checks every design of every kind: each non-flat stat reads one declared role,
declared roles are read, sellers author each once, role-mapped designs are not all flat (the tool answers
which, via Program.HasRoleMap). RolesBackfillIsAuthored pins ratios and roles, with the Autocannon
Penetration, Mine Range and LRMM72 LockAngle asserts. RolesBackfillCommandTests resets the backfilled designs
on a scratch catalog, applies, and expects the shipped catalog back, then replays as a no-op.
EveryShippedProductHasAMaker. TheEnvelopeComesFromTheGear bounds each drive by the catalog's Large Drive
Thrust range; ShippedWeaponOperabilityTests reads the catalog's DefaultEntitySettings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 23:46:28 +02:00
} ;
private readonly string _root = Path . Combine ( Path . GetTempPath ( ) , "roles-backfill-" + Guid . NewGuid ( ) . ToString ( "N" ) ) ;
private string CatalogPath = > Path . Combine ( _root , "GameData" , "Aetheria.cc" ) ;
private readonly Dictionary < string , string > _shipped = new Dictionary < string , string > ( ) ;
public RolesBackfillCommandTests ( )
{
Directory . CreateDirectory ( Path . Combine ( _root , "GameData" ) ) ;
File . Copy ( Path . Combine ( AetherDb . FindRoot ( ) , "GameData" , "Aetheria.cc" ) , CatalogPath ) ;
2026-10-10 06:50:26 +02:00
using var cache = new CultCache ( TestCatalog . Registry ( ) ) ;
2026-10-09 00:36:45 +02:00
cache . AddBackingStore ( new SingleFileMessagePackBackingStore ( CatalogPath ) , AetheriaStores . CatalogTypes ) ;
var products = cache . GetAll < FactionProductData > ( ) . ToArray ( ) ;
foreach ( var design in cache . GetAll < EquippableItemData > ( ) . Where ( d = > Flattened . Contains ( d . Name ) | | Unnamed . Any ( u = > u . Design = = d . Name ) ) )
_shipped [ design . Name ] = Describe ( cache , design , products ) ;
2026-10-09 00:24:58 +02:00
}
2026-10-09 00:36:45 +02:00
// A registry scoped to the shipped assembly, so this test assembly's own documents never reach the real catalog.
2026-10-09 00:24:58 +02:00
// Puts the named designs back to the state they shipped in before the backfill, through a registry scoped to the shipped
// assembly (as PiratesFactionCommandTests strips its faction).
private void Reset ( bool flattened , bool unnamed )
{
2026-10-10 06:50:26 +02:00
using var cache = new CultCache ( TestCatalog . Registry ( ) ) ;
2026-10-09 00:24:58 +02:00
cache . AddBackingStore ( new SingleFileMessagePackBackingStore ( CatalogPath ) , AetheriaStores . CatalogTypes ) ;
var designs = cache . GetAll < EquippableItemData > ( ) . ToArray ( ) ;
var products = cache . GetAll < FactionProductData > ( ) . ToArray ( ) ;
var changed = new List < object > ( ) ;
if ( flattened )
Tests: CB-R1 enforced whole, backfill replay on a scratch catalog, envelope and operability read the catalog
ShippedCatalogRolesAreLive checks every design of every kind: each non-flat stat reads one declared role,
declared roles are read, sellers author each once, role-mapped designs are not all flat (the tool answers
which, via Program.HasRoleMap). RolesBackfillIsAuthored pins ratios and roles, with the Autocannon
Penetration, Mine Range and LRMM72 LockAngle asserts. RolesBackfillCommandTests resets the backfilled designs
on a scratch catalog, applies, and expects the shipped catalog back, then replays as a no-op.
EveryShippedProductHasAMaker. TheEnvelopeComesFromTheGear bounds each drive by the catalog's Large Drive
Thrust range; ShippedWeaponOperabilityTests reads the catalog's DefaultEntitySettings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 23:46:28 +02:00
foreach ( var design in designs . Where ( d = > Flattened . Contains ( d . Name ) ) )
{
foreach ( var ( _ , stat ) in CatalogRoleTests . StatsOf ( design ) )
{
if ( stat . Min ! = stat . Max ) stat . Min = stat . Max = ( stat . Min + stat . Max ) / 2f ;
2026-10-09 02:13:15 +02:00
// The drives shipped with unnamed Quality terms on their flat stats (roles-migrate); the command keeps those
// exponents and names them. The other four shipped with no term at all.
if ( design . Name . EndsWith ( " Drive" , StringComparison . Ordinal ) ) foreach ( var term in stat . Terms . Where ( t = > t . Source = = StatSource . Quality ) ) term . Role = null ;
else stat . Terms . RemoveAll ( t = > t . Source = = StatSource . Quality ) ;
Tests: CB-R1 enforced whole, backfill replay on a scratch catalog, envelope and operability read the catalog
ShippedCatalogRolesAreLive checks every design of every kind: each non-flat stat reads one declared role,
declared roles are read, sellers author each once, role-mapped designs are not all flat (the tool answers
which, via Program.HasRoleMap). RolesBackfillIsAuthored pins ratios and roles, with the Autocannon
Penetration, Mine Range and LRMM72 LockAngle asserts. RolesBackfillCommandTests resets the backfilled designs
on a scratch catalog, applies, and expects the shipped catalog back, then replays as a no-op.
EveryShippedProductHasAMaker. TheEnvelopeComesFromTheGear bounds each drive by the catalog's Large Drive
Thrust range; ShippedWeaponOperabilityTests reads the catalog's DefaultEntitySettings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 23:46:28 +02:00
}
design . Roles = new List < ItemRole > ( ) ;
changed . Add ( design ) ;
2026-10-08 23:50:14 +02:00
// Small Drive shipped with its sellers' quality already authored (roles-migrate); only its stats were flat.
foreach ( var product in products . Where ( p = > design . Name ! = "Small Drive" & & p . Design . Key . Equals ( cache . RefOf ( design ) . Key ) ) )
Tests: CB-R1 enforced whole, backfill replay on a scratch catalog, envelope and operability read the catalog
ShippedCatalogRolesAreLive checks every design of every kind: each non-flat stat reads one declared role,
declared roles are read, sellers author each once, role-mapped designs are not all flat (the tool answers
which, via Program.HasRoleMap). RolesBackfillIsAuthored pins ratios and roles, with the Autocannon
Penetration, Mine Range and LRMM72 LockAngle asserts. RolesBackfillCommandTests resets the backfilled designs
on a scratch catalog, applies, and expects the shipped catalog back, then replays as a no-op.
EveryShippedProductHasAMaker. TheEnvelopeComesFromTheGear bounds each drive by the catalog's Large Drive
Thrust range; ShippedWeaponOperabilityTests reads the catalog's DefaultEntitySettings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 23:46:28 +02:00
{
product . Roles = new List < ProductRole > ( ) ;
changed . Add ( product ) ;
}
}
2026-10-09 00:24:58 +02:00
if ( unnamed )
Tests: CB-R1 enforced whole, backfill replay on a scratch catalog, envelope and operability read the catalog
ShippedCatalogRolesAreLive checks every design of every kind: each non-flat stat reads one declared role,
declared roles are read, sellers author each once, role-mapped designs are not all flat (the tool answers
which, via Program.HasRoleMap). RolesBackfillIsAuthored pins ratios and roles, with the Autocannon
Penetration, Mine Range and LRMM72 LockAngle asserts. RolesBackfillCommandTests resets the backfilled designs
on a scratch catalog, applies, and expects the shipped catalog back, then replays as a no-op.
EveryShippedProductHasAMaker. TheEnvelopeComesFromTheGear bounds each drive by the catalog's Large Drive
Thrust range; ShippedWeaponOperabilityTests reads the catalog's DefaultEntitySettings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 23:46:28 +02:00
foreach ( var ( name , field ) in Unnamed )
{
var design = designs . Single ( d = > d . Name = = name ) ;
foreach ( var term in Stat ( design , field ) . Terms . Where ( t = > t . Source = = StatSource . Quality ) ) term . Role = null ;
changed . Add ( design ) ;
}
2026-10-09 00:36:45 +02:00
if ( changed . Count > 0 )
cache . Commit ( batch = >
{
foreach ( var document in changed ) batch . Upsert ( document . GetType ( ) , document , cache . RefOf ( document ) . Key ) ;
} ) ;
2026-10-09 00:24:58 +02:00
cache . FlushAsync ( ) . Wait ( ) ;
2026-10-09 00:36:45 +02:00
// Then the record AetherDb's process-wide registry demands in this test host (as PiratesFactionCommandTests).
2026-10-09 00:24:58 +02:00
using ( var plain = new CultCache ( ) )
{
plain . AddBackingStore ( new SingleFileMessagePackBackingStore ( CatalogPath ) , AetheriaStores . CatalogTypes ) ;
2026-10-10 07:18:00 +02:00
plain . Upsert ( new VerseGrammar { Revision = 1 } ) ;
2026-10-09 00:24:58 +02:00
plain . FlushAsync ( ) . Wait ( ) ;
Tests: CB-R1 enforced whole, backfill replay on a scratch catalog, envelope and operability read the catalog
ShippedCatalogRolesAreLive checks every design of every kind: each non-flat stat reads one declared role,
declared roles are read, sellers author each once, role-mapped designs are not all flat (the tool answers
which, via Program.HasRoleMap). RolesBackfillIsAuthored pins ratios and roles, with the Autocannon
Penetration, Mine Range and LRMM72 LockAngle asserts. RolesBackfillCommandTests resets the backfilled designs
on a scratch catalog, applies, and expects the shipped catalog back, then replays as a no-op.
EveryShippedProductHasAMaker. TheEnvelopeComesFromTheGear bounds each drive by the catalog's Large Drive
Thrust range; ShippedWeaponOperabilityTests reads the catalog's DefaultEntitySettings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 23:46:28 +02:00
}
}
public void Dispose ( ) = > Directory . Delete ( _root , true ) ;
private static PerformanceStat Stat ( EquippableItemData design , string field ) = >
Assert . Single ( CatalogRoleTests . StatsOf ( design ) , s = > s . Name . EndsWith ( "." + field ) ) . Stat ;
2026-10-09 01:59:25 +02:00
// Everything the command decides about one design: its roles, each stat's range, role and exponent, and its sellers' role quality.
Tests: CB-R1 enforced whole, backfill replay on a scratch catalog, envelope and operability read the catalog
ShippedCatalogRolesAreLive checks every design of every kind: each non-flat stat reads one declared role,
declared roles are read, sellers author each once, role-mapped designs are not all flat (the tool answers
which, via Program.HasRoleMap). RolesBackfillIsAuthored pins ratios and roles, with the Autocannon
Penetration, Mine Range and LRMM72 LockAngle asserts. RolesBackfillCommandTests resets the backfilled designs
on a scratch catalog, applies, and expects the shipped catalog back, then replays as a no-op.
EveryShippedProductHasAMaker. TheEnvelopeComesFromTheGear bounds each drive by the catalog's Large Drive
Thrust range; ShippedWeaponOperabilityTests reads the catalog's DefaultEntitySettings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 23:46:28 +02:00
private static string Describe ( CultCache cache , EquippableItemData design , FactionProductData [ ] products )
{
static string F ( float v ) = > float . IsInfinity ( v ) ? v . ToString ( ) : ( ( double ) v ) . ToString ( "G5" ) ;
var stats = CatalogRoleTests . StatsOf ( design ) . Select ( s = >
2026-10-09 01:59:25 +02:00
$"{s.Name} {F(s.Stat.Min)}..{F(s.Stat.Max)} {string.Join(" + ", s.Stat.Terms.Where(t => t.Source == StatSource.Quality && !string.IsNullOrEmpty(t.Role)).Select(t => $" { t . Role } ^ { F ( t . Exponent ) } "))}" ) ;
Tests: CB-R1 enforced whole, backfill replay on a scratch catalog, envelope and operability read the catalog
ShippedCatalogRolesAreLive checks every design of every kind: each non-flat stat reads one declared role,
declared roles are read, sellers author each once, role-mapped designs are not all flat (the tool answers
which, via Program.HasRoleMap). RolesBackfillIsAuthored pins ratios and roles, with the Autocannon
Penetration, Mine Range and LRMM72 LockAngle asserts. RolesBackfillCommandTests resets the backfilled designs
on a scratch catalog, applies, and expects the shipped catalog back, then replays as a no-op.
EveryShippedProductHasAMaker. TheEnvelopeComesFromTheGear bounds each drive by the catalog's Large Drive
Thrust range; ShippedWeaponOperabilityTests reads the catalog's DefaultEntitySettings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 23:46:28 +02:00
var sellers = products . Where ( p = > p . Design . Key . Equals ( cache . RefOf ( design ) . Key ) ) . OrderBy ( p = > p . Name , StringComparer . Ordinal )
. Select ( p = > $"{p.Name}: " + string . Join ( ", " , ( p . Roles ? ? new List < ProductRole > ( ) ) . OrderBy ( r = > r . Role , StringComparer . Ordinal )
. Select ( r = > $"{r.Role} {F(r.Mean)}/{F(r.StandardDeviation)}" ) ) ) ;
var roles = "roles " + string . Join ( ", " , ( design . Roles ? ? new List < ItemRole > ( ) ) . Select ( r = > r . Name ) . OrderBy ( r = > r , StringComparer . Ordinal ) ) ;
return string . Join ( "\n" , new [ ] { roles } . Concat ( stats ) . Concat ( sellers ) ) ;
}
private Dictionary < string , string > Read ( )
{
var db = AetherDb . Open ( root : _root ) ;
try
{
var products = db . Cache . GetAll < FactionProductData > ( ) . ToArray ( ) ;
return db . Cache . GetAll < EquippableItemData > ( ) . Where ( d = > _shipped . ContainsKey ( d . Name ) )
. ToDictionary ( d = > d . Name , d = > Describe ( db . Cache , d , products ) ) ;
}
finally { db . Cache . Dispose ( ) ; }
}
[Fact]
public void A_dry_run_writes_nothing_and_apply_lands_what_the_catalog_ships_and_replays_as_a_no_op ( )
{
2026-10-09 00:24:58 +02:00
Reset ( flattened : true , unnamed : true ) ;
Tests: CB-R1 enforced whole, backfill replay on a scratch catalog, envelope and operability read the catalog
ShippedCatalogRolesAreLive checks every design of every kind: each non-flat stat reads one declared role,
declared roles are read, sellers author each once, role-mapped designs are not all flat (the tool answers
which, via Program.HasRoleMap). RolesBackfillIsAuthored pins ratios and roles, with the Autocannon
Penetration, Mine Range and LRMM72 LockAngle asserts. RolesBackfillCommandTests resets the backfilled designs
on a scratch catalog, applies, and expects the shipped catalog back, then replays as a no-op.
EveryShippedProductHasAMaker. TheEnvelopeComesFromTheGear bounds each drive by the catalog's Large Drive
Thrust range; ShippedWeaponOperabilityTests reads the catalog's DefaultEntitySettings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 23:46:28 +02:00
var before = File . ReadAllBytes ( CatalogPath ) ;
var start = Read ( ) ;
Assert . All ( _shipped . Keys , name = > Assert . NotEqual ( _shipped [ name ] , start [ name ] ) ) ;
Assert . Equal ( 0 , Program . RolesBackfill ( apply : false , root : _root ) ) ;
Assert . Equal ( before , File . ReadAllBytes ( CatalogPath ) ) ;
Assert . Equal ( 0 , Program . RolesBackfill ( apply : true , root : _root ) ) ;
var landed = File . ReadAllBytes ( CatalogPath ) ;
Assert . NotEqual ( before , landed ) ;
var after = Read ( ) ;
foreach ( var name in _shipped . Keys . OrderBy ( n = > n , StringComparer . Ordinal ) )
Assert . Equal ( _shipped [ name ] , after [ name ] ) ;
// A second apply finds every stat ranged and named and every seller authored, and writes nothing.
Assert . Equal ( 0 , Program . RolesBackfill ( apply : true , root : _root ) ) ;
Assert . Equal ( landed , File . ReadAllBytes ( CatalogPath ) ) ;
}
2026-10-09 00:24:58 +02:00
// Only unnamed Quality terms to name: no seller and no range changes, and apply must still land the designs.
[Fact]
public void Naming_an_unnamed_term_alone_lands_without_touching_ranges_or_sellers ( )
{
Reset ( flattened : false , unnamed : true ) ;
var before = File . ReadAllBytes ( CatalogPath ) ;
var start = Read ( ) ;
Assert . All ( Unnamed . Select ( u = > u . Design ) , name = > Assert . NotEqual ( _shipped [ name ] , start [ name ] ) ) ;
Assert . Equal ( 0 , Program . RolesBackfill ( apply : true , root : _root ) ) ;
Assert . NotEqual ( before , File . ReadAllBytes ( CatalogPath ) ) ;
var after = Read ( ) ;
foreach ( var ( name , _ ) in Unnamed ) Assert . Equal ( _shipped [ name ] , after [ name ] ) ;
}
2026-10-09 01:59:25 +02:00
// The exponents a first apply authors do not depend on which designs it targets: applied to the base-shaped catalog, the
// command reproduces the shipped exponents of Autocannon and LRMM72, which differ across stats (the mode is not one value).
[Fact]
public void Apply_on_the_base_shaped_catalog_reproduces_the_shipped_exponents ( )
{
static Dictionary < string , float > Exponents ( EquippableItemData d ) = > CatalogRoleTests . StatsOf ( d )
. SelectMany ( s = > s . Stat . Terms . Where ( t = > t . Source = = StatSource . Quality & & ! string . IsNullOrEmpty ( t . Role ) ) . Select ( t = > ( s . Name , t . Exponent ) ) )
. ToDictionary ( x = > x . Name , x = > x . Exponent ) ;
2026-10-13 20:29:10 +02:00
var designs = new [ ] { "Autocannon" , "Long-Range Missile Rack" } ;
2026-10-09 02:14:54 +02:00
Dictionary < string , Dictionary < string , float > > shipped ;
Dictionary < string , Dictionary < string , float > > Read ( EquippableItemData [ ] all ) = > all . Where ( d = > designs . Contains ( d . Name ) ) . ToDictionary ( d = > d . Name , Exponents ) ;
using ( var catalog = RestoredHullsTests . OpenCatalog ( ) ) shipped = Read ( catalog . GetAll < EquippableItemData > ( ) . ToArray ( ) ) ;
2026-10-09 01:59:25 +02:00
Assert . All ( designs , name = > Assert . True ( shipped [ name ] . Values . Distinct ( ) . Count ( ) > 1 , name + " exponents are one value; the test would not tell a mode from a default" ) ) ;
Reset ( flattened : true , unnamed : true ) ;
Assert . Equal ( 0 , Program . RolesBackfill ( apply : true , root : _root ) ) ;
2026-10-09 02:14:54 +02:00
var db = AetherDb . Open ( root : _root ) ;
Dictionary < string , Dictionary < string , float > > after ;
try { after = Read ( db . Cache . GetAll < EquippableItemData > ( ) . ToArray ( ) ) ; }
finally { db . Cache . Dispose ( ) ; }
2026-10-09 01:59:25 +02:00
foreach ( var name in designs ) Assert . Equal ( shipped [ name ] . OrderBy ( x = > x . Key , StringComparer . Ordinal ) , after [ name ] . OrderBy ( x = > x . Key , StringComparer . Ordinal ) ) ;
}
// A seller that authors one of a design's two roles gets only the other one added, and keeps the one it authored.
[Fact]
public void A_seller_with_one_of_two_roles_authored_gains_only_the_missing_role ( )
{
Reset ( flattened : false , unnamed : false ) ;
var db = AetherDb . Open ( catalogWritable : true , root : _root ) ;
var autocannon = db . Cache . GetAll < WeaponItemData > ( ) . Single ( weapon = > weapon . Name = = "Autocannon" ) ;
var seller = db . Cache . GetAll < FactionProductData > ( ) . OrderBy ( p = > p . Name , StringComparer . Ordinal ) . First ( p = > p . Design . Key . Equals ( db . Cache . RefOf ( autocannon ) . Key ) & & p . Roles . Count = = 2 ) ;
var kept = seller . Roles . OrderBy ( r = > r . Role , StringComparer . Ordinal ) . First ( ) ;
kept . Mean = . 91f ;
seller . Roles = new List < ProductRole > { kept } ;
var sellerName = seller . Name ;
var keptRole = kept . Role ;
db . Cache . Commit ( batch = > batch . Upsert ( typeof ( FactionProductData ) , seller , db . Cache . RefOf ( seller ) . Key ) ) ;
db . Cache . Dispose ( ) ;
Assert . Equal ( 0 , Program . RolesBackfill ( apply : true , root : _root ) ) ;
db = AetherDb . Open ( root : _root ) ;
try
{
var after = db . Cache . GetAll < FactionProductData > ( ) . Single ( p = > p . Name = = sellerName ) ;
Assert . Equal ( new [ ] { "barrel" , "feed mechanism" } , after . Roles . Select ( r = > r . Role ) . OrderBy ( r = > r , StringComparer . Ordinal ) . ToArray ( ) ) ;
Assert . Equal ( . 91f , after . Roles . Single ( r = > r . Role = = keptRole ) . Mean , 3 ) ;
}
finally { db . Cache . Dispose ( ) ; }
}
2026-10-18 18:06:07 +02:00
// A bin authors no roles (ProductBin): the backfill that restores its line's missing roles leaves the bin's list empty.
[Fact]
public void A_binned_seller_gains_no_roles ( )
{
Reset ( flattened : false , unnamed : false ) ;
var db = AetherDb . Open ( catalogWritable : true , root : _root ) ;
var autocannon = db . Cache . GetAll < WeaponItemData > ( ) . Single ( weapon = > weapon . Name = = "Autocannon" ) ;
var seller = db . Cache . GetAll < FactionProductData > ( ) . OrderBy ( p = > p . Name , StringComparer . Ordinal ) . First ( p = > p . Design . Key . Equals ( db . Cache . RefOf ( autocannon ) . Key ) & & p . Roles . Count = = 2 ) ;
var sellerName = seller . Name ;
seller . Roles = new List < ProductRole > { seller . Roles . OrderBy ( r = > r . Role , StringComparer . Ordinal ) . First ( ) } ;
var bin = new FactionProductData
{
Name = "Binned " + sellerName , Design = seller . Design , Manufacturer = seller . Manufacturer ,
Bin = new ProductBin { Line = db . Cache . RefOf ( seller ) , Floor = 0f }
} ;
db . Cache . Commit ( batch = >
{
batch . Upsert ( typeof ( FactionProductData ) , seller , db . Cache . RefOf ( seller ) . Key ) ;
batch . Upsert ( typeof ( FactionProductData ) , bin ) ;
} ) ;
db . Cache . Dispose ( ) ;
Assert . Equal ( 0 , Program . RolesBackfill ( apply : true , root : _root ) ) ;
db = AetherDb . Open ( root : _root ) ;
try
{
Assert . Equal ( 2 , db . Cache . GetAll < FactionProductData > ( ) . Single ( p = > p . Name = = sellerName ) . Roles . Count ) ;
Assert . Empty ( db . Cache . GetAll < FactionProductData > ( ) . Single ( p = > p . Name = = "Binned " + sellerName ) . Roles ) ;
}
finally { db . Cache . Dispose ( ) ; }
}
2026-10-18 18:46:05 +02:00
// The census reports a seller that authors no spread for a role of its design as a gap, and never a bin: a bin reads its line's.
[Fact]
public void The_census_reports_a_roleless_seller_and_never_a_bin ( )
{
var db = AetherDb . Open ( catalogWritable : true , root : _root ) ;
var autocannon = db . Cache . GetAll < WeaponItemData > ( ) . Single ( weapon = > weapon . Name = = "Autocannon" ) ;
var seller = db . Cache . GetAll < FactionProductData > ( ) . OrderBy ( p = > p . Name , StringComparer . Ordinal ) . First ( p = > p . Design . Key . Equals ( db . Cache . RefOf ( autocannon ) . Key ) ) ;
var sellerName = seller . Name ;
seller . Roles = new List < ProductRole > ( ) ;
db . Cache . Commit ( batch = >
{
batch . Upsert ( typeof ( FactionProductData ) , seller , db . Cache . RefOf ( seller ) . Key ) ;
batch . Upsert ( typeof ( FactionProductData ) , new FactionProductData
{
Name = "Binned " + sellerName , Design = seller . Design , Manufacturer = seller . Manufacturer ,
Bin = new ProductBin { Line = db . Cache . RefOf ( seller ) , Floor = 0f }
} ) ;
} ) ;
db . Cache . Dispose ( ) ;
db = AetherDb . Open ( root : _root ) ;
try
{
var gaps = Program . RoleGaps ( db . Cache , db . Cache . GetAll < EquippableItemData > ( ) . ToArray ( ) , db . Cache . GetAll < FactionProductData > ( ) . ToArray ( ) ) . ToArray ( ) ;
Assert . Contains ( gaps , gap = > gap . Product . Name = = sellerName & & gap . Missing . Length = = 2 ) ;
Assert . DoesNotContain ( gaps , gap = > gap . Product . Name = = "Binned " + sellerName ) ;
}
finally { db . Cache . Dispose ( ) ; }
}
2026-10-18 18:06:07 +02:00
// roles-migrate authors a role on every product that sells a design lacking one; a bin sells none of its own.
[Fact]
public void A_binned_seller_gains_no_roles_from_roles_migrate ( )
{
var db = AetherDb . Open ( catalogWritable : true , root : _root ) ;
var autocannon = db . Cache . GetAll < WeaponItemData > ( ) . Single ( weapon = > weapon . Name = = "Autocannon" ) ;
var seller = db . Cache . GetAll < FactionProductData > ( ) . OrderBy ( p = > p . Name , StringComparer . Ordinal ) . First ( p = > p . Design . Key . Equals ( db . Cache . RefOf ( autocannon ) . Key ) ) ;
var bin = new FactionProductData
{
Name = "Binned " + seller . Name , Design = seller . Design , Manufacturer = seller . Manufacturer ,
Bin = new ProductBin { Line = db . Cache . RefOf ( seller ) , Floor = 0f }
} ;
db . Cache . Commit ( batch = > batch . Upsert ( typeof ( FactionProductData ) , bin ) ) ;
db . Cache . Dispose ( ) ;
Assert . Equal ( 0 , Program . RolesMigrate ( apply : true , root : _root ) ) ;
db = AetherDb . Open ( root : _root ) ;
try { Assert . Empty ( db . Cache . GetAll < FactionProductData > ( ) . Single ( p = > p . Name = = "Binned " + seller . Name ) . Roles ) ; }
finally { db . Cache . Dispose ( ) ; }
}
2026-10-18 21:08:11 +02:00
// sensor-stat-set authors an emission-discipline role on every product of a sensor design; a bin of one authors none.
[Fact]
public void A_binned_seller_gains_no_roles_from_sensor_stat_set ( )
{
var db = AetherDb . Open ( catalogWritable : true , root : _root ) ;
var sensor = db . Cache . GetAll < EquippableItemData > ( ) . Where ( d = > d . Behaviors ! = null & & d . Behaviors . OfType < SensorData > ( ) . Any ( ) ) . OrderBy ( d = > d . Name , StringComparer . Ordinal ) . First ( ) ;
var seller = db . Cache . GetAll < FactionProductData > ( ) . OrderBy ( p = > p . Name , StringComparer . Ordinal ) . First ( p = > p . Bin = = null & & p . Design . Key . Equals ( db . Cache . RefOf ( sensor ) . Key ) ) ;
var bin = new FactionProductData
{
Name = "Binned " + seller . Name , Design = seller . Design , Manufacturer = seller . Manufacturer ,
Bin = new ProductBin { Line = db . Cache . RefOf ( seller ) , Floor = 0f }
} ;
db . Cache . Commit ( batch = > batch . Upsert ( typeof ( FactionProductData ) , bin ) ) ;
db . Cache . Dispose ( ) ;
Assert . Equal ( 0 , Program . SensorStatSet ( apply : true , root : _root ) ) ;
db = AetherDb . Open ( root : _root ) ;
try { Assert . Empty ( db . Cache . GetAll < FactionProductData > ( ) . Single ( p = > p . Name = = "Binned " + seller . Name ) . Roles ) ; }
finally { db . Cache . Dispose ( ) ; }
}
Tests: CB-R1 enforced whole, backfill replay on a scratch catalog, envelope and operability read the catalog
ShippedCatalogRolesAreLive checks every design of every kind: each non-flat stat reads one declared role,
declared roles are read, sellers author each once, role-mapped designs are not all flat (the tool answers
which, via Program.HasRoleMap). RolesBackfillIsAuthored pins ratios and roles, with the Autocannon
Penetration, Mine Range and LRMM72 LockAngle asserts. RolesBackfillCommandTests resets the backfilled designs
on a scratch catalog, applies, and expects the shipped catalog back, then replays as a no-op.
EveryShippedProductHasAMaker. TheEnvelopeComesFromTheGear bounds each drive by the catalog's Large Drive
Thrust range; ShippedWeaponOperabilityTests reads the catalog's DefaultEntitySettings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 23:46:28 +02:00
[Fact]
public void A_design_the_catalog_does_not_hold_refuses_and_lands_nothing ( )
{
2026-10-09 00:36:45 +02:00
Reset ( flattened : false , unnamed : false ) ;
Tests: CB-R1 enforced whole, backfill replay on a scratch catalog, envelope and operability read the catalog
ShippedCatalogRolesAreLive checks every design of every kind: each non-flat stat reads one declared role,
declared roles are read, sellers author each once, role-mapped designs are not all flat (the tool answers
which, via Program.HasRoleMap). RolesBackfillIsAuthored pins ratios and roles, with the Autocannon
Penetration, Mine Range and LRMM72 LockAngle asserts. RolesBackfillCommandTests resets the backfilled designs
on a scratch catalog, applies, and expects the shipped catalog back, then replays as a no-op.
EveryShippedProductHasAMaker. TheEnvelopeComesFromTheGear bounds each drive by the catalog's Large Drive
Thrust range; ShippedWeaponOperabilityTests reads the catalog's DefaultEntitySettings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 23:46:28 +02:00
var db = AetherDb . Open ( catalogWritable : true , root : _root ) ;
var autocannon = db . Cache . GetAll < WeaponItemData > ( ) . Single ( weapon = > weapon . Name = = "Autocannon" ) ;
autocannon . Name = "Autocannon (renamed)" ;
db . Cache . Commit ( batch = > batch . Upsert ( typeof ( WeaponItemData ) , autocannon , db . Cache . RefOf ( autocannon ) . Key ) ) ;
db . Cache . Dispose ( ) ;
var before = File . ReadAllBytes ( CatalogPath ) ;
Assert . Throws < InvalidOperationException > ( ( ) = > Program . RolesBackfill ( apply : true , root : _root ) ) ;
Assert . Equal ( before , File . ReadAllBytes ( CatalogPath ) ) ;
}
}