Aetheria/tests/Aetheria.Shared.Tests/SignatureTests.cs
Meta Vens c47ced9cfd Paints carry search or track; a ping sweep warns every ship it crosses in its beam; allies hear the return silently
LockWarning becomes PaintWarning with a PaintMode (Search for a ping sweep, Track for a lock);
Entity.IncomingLocks becomes IncomingPaints. Sensor replaces the _pingedEntities set with a stateless
crossing rule over the previous and current wavefront radius, limited to a new BeamWidth stat (key 12,
absent reads 360), and credits allied pings from the receiving sensor's own sensitivity and distance.
The HUD lock indicator follows Track paints only. Folds in the radiator sampling-instant pin.

Unverified at this commit (first build still to run).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:05:16 +02:00

569 lines
25 KiB
C#

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using CultMath;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using MessagePack;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
using static CultMath.math;
using float2 = CultMath.float2;
using float3 = CultMath.float3;
using int2 = CultMath.int2;
// Visibility cut 1 (ruling visibility-per-capability-reveal): every light signature names the channel it travels
// on and the item that produced it, through the one writer Entity.SetSignature, and Visibility stays the sum. No
// number changes. Each fixture is the production path the rule is about: a real ship, real gear, real ticks.
public sealed class SignatureTests : IDisposable
{
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-signature-" + Guid.NewGuid().ToString("N"));
private readonly List<CultCache> _caches = new List<CultCache>();
public SignatureTests() => Directory.CreateDirectory(_root);
public void Dispose()
{
foreach (var c in _caches) c.Dispose();
Directory.Delete(_root, true);
}
private static PerformanceStat Constant(float v) => new PerformanceStat { Min = v, Max = v };
// Per-test authoring of the numbers a rule is a function of (all default to the values the other tests rely on).
private float _weaponVisibility = 100f;
private float _radiationMultiplier = 0f; // the hull radiates nothing unless a test says so
private float _radiationExponent = 1f;
private GameplaySettings Settings(float visibilityDecay) => new GameplaySettings
{
DefaultEntitySettings = new EntitySettings(),
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = .5f,
SchematicCellSize = 2f,
UnaidedAccuracy = .05f,
UnaidedTracking = 10f,
UnaidedPrecision = 1000f,
AgentMinHitProbability = .2f,
BeamResolveInterval = .1f,
VisibilityDecay = visibilityDecay,
// The hull radiates nothing by default, so every sum below is the sum of the authored constants.
HeatRadiationMultiplier = _radiationMultiplier,
HeatRadiationExponent = _radiationExponent
};
private static GearData Gear(string name, BehaviorData behavior) => new GearData
{
Name = name, Hardpoint = HardpointType.Tool, Shape = new Shape(), Durability = 10,
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000,
Behaviors = { behavior }
};
private enum Armament { None, Gun, Beam }
private sealed class Rig
{
public Zone Zone;
public Ship Ship;
public EquippedItem WeaponItem;
public Weapon Weapon;
}
private static Shape Solid(int w, int h)
{
var shape = new Shape(w, h);
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
return shape;
}
// A 5x5 hull with a reactor and, optionally, a fused gun or beam (it fires at no target: Range 40 outruns its
// arming distance 30), then the given gear. A thruster goes on the centre column so it needs no torque.
private Rig Build(Armament armament, float visibilityDecay, ThrusterData thruster, params BehaviorData[] gear)
{
var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), catalogWritable: true);
_caches.Add(cache);
cache.Upsert(new VerseGrammar { Revision = 1 });
var hull = new HullData
{
Name = "Skiff", HullType = HullType.Ship, Shape = Solid(5, 5), Durability = 1000000, Mass = 1000,
Hardpoints = { new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 0), Shape = new Shape() } }
};
cache.Upsert(hull);
cache.Upsert(Gear("Reactor", new ReactorData { Charge = Constant(1000), Efficiency = Constant(1), OverloadEfficiency = Constant(1), ThrottlingFactor = Constant(2) }));
cache.Upsert(Gear("Targeting", new TargetingSystemData { Accuracy = Constant(1), Resolution = Constant(1000f), Precision = Constant(1000f), Tracking = Constant(1000000f) }));
if (armament != Armament.None)
{
WeaponData behavior = armament == Armament.Beam
? new ConstantWeaponData()
: new InstantWeaponData { Count = Constant(1), BurstTime = Constant(0), Cooldown = Constant(1) };
behavior.Damage = Constant(100);
behavior.Range = Constant(40);
behavior.MinRange = Constant(0);
behavior.Velocity = Constant(0);
behavior.Spread = Constant(0);
behavior.DamageSpread = Constant(0);
behavior.Penetration = Constant(0);
behavior.Energy = Constant(50);
behavior.Heat = Constant(0);
behavior.Visibility = Constant(_weaponVisibility);
behavior.MagazineSize = armament == Armament.Gun ? 5 : 0;
behavior.DamageCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } };
cache.Upsert(new WeaponItemData
{
Name = "Gun", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 1,
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000,
Fuse = WeaponFuse.Proximity, BlastRadius = 30f,
Behaviors = { behavior }
});
}
if (thruster != null) cache.Upsert(Gear("Thruster", thruster));
for (var i = 0; i < gear.Length; i++) cache.Upsert(Gear("Gear" + i, gear[i]));
cache.FlushAsync().Wait();
var ledger = new ProvenanceLedger();
for (var i = 1; i <= 100; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 };
var items = new ItemManager(cache, ledger, Settings(visibilityDecay), _ => { });
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Signatures", Owner = null }, null);
var lot = 1;
EquippableItem Make(string name) => new EquippableItem
{
Data = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<ItemData>(name)), Durability = 10, Lot = lot++
};
var ship = new Ship(items, zone, new EquippableItem { Data = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>("Skiff")), Durability = 1000000, Lot = lot++ }, new EntitySettings());
var rig = new Rig { Zone = zone, Ship = ship };
if (armament != Armament.None)
{
var gun = Make("Gun");
Assert.True(ship.TryEquip(gun, new int2(0, 0)));
rig.WeaponItem = ship.Equipment.Single(x => x.EquippableItem == gun);
rig.Weapon = (Weapon) rig.WeaponItem.Behaviors.Single(b => b is Weapon);
}
if (thruster != null) Assert.True(ship.TryEquip(Make("Thruster"), new int2(2, 1)));
foreach (var name in new[] { "Targeting", "Reactor" }.Concat(gear.Select((_, i) => "Gear" + i)))
{
var item = Make(name);
Assert.True(ship.TryFindSpace(item, out var at));
Assert.True(ship.TryEquip(item, at));
}
zone.Entities.Add(ship);
ship.Activate();
ship.Aim = float3(0, 0, 1);
zone.Update(1f);
return rig;
}
private static void Fire(Rig r)
{
if (r.Weapon is InstantWeapon gun)
{
var capacitor = (InputCapacitor) typeof(InstantWeapon).GetField("_capacitor", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(gun);
capacitor.AddCharge(1000f);
}
r.Ship.SetTarget(TargetRef.None);
r.Weapon.Activate();
for (var i = 0; i < 5; i++) r.Zone.Update(.1f);
}
private static Signature Of(Ship ship, object source) => ship.VisibilitySources[source];
private static T Single<T>(Ship ship) where T : Behavior => ship.GetBehavior<T>();
// ---- EveryWriterNamesItsChannel ----
// The channel is the writer's meaning, not the strength's: a strong flash and one below any plausible threshold
// travel on the same channel.
[Theory]
[InlineData(100f)]
[InlineData(.5f)]
public void AFiredGunShowsALaunchFlashWithItsItem(float visibility)
{
_weaponVisibility = visibility;
var r = Build(Armament.Gun, 0f, null);
Fire(r);
var s = Of(r.Ship, r.Weapon);
Assert.Equal(SignatureChannel.LaunchFlash, s.Channel);
Assert.Same(r.WeaponItem, s.Item);
Assert.Equal(visibility, s.Strength, 3);
}
[Theory]
[InlineData(100f)]
[InlineData(.5f)]
public void ABurningBeamShowsALaunchFlashWithItsItem(float visibility)
{
_weaponVisibility = visibility;
var r = Build(Armament.Beam, 0f, null);
Fire(r);
var s = Of(r.Ship, r.Weapon);
Assert.Equal(SignatureChannel.LaunchFlash, s.Channel);
Assert.Same(r.WeaponItem, s.Item);
Assert.Equal(visibility, s.Strength, 3);
}
private static ThrusterData Drive(float visibility) =>
new ThrusterData { Thrust = Constant(100), Visibility = Constant(visibility), Heat = Constant(0), EnergyUsage = Constant(10) };
private static void Burn(Rig r, float input)
{
r.Ship.MovementDirection = float2(0, -input);
r.Zone.Update(.1f);
}
[Theory]
[InlineData(10f)]
[InlineData(.5f)]
public void ABurningThrusterShowsADrivePlumeWithItsItem(float visibility)
{
var r = Build(Armament.None, 0f, Drive(visibility));
var thruster = Single<Thruster>(r.Ship);
thruster.Item.UpdatePerformance();
Burn(r, 1f);
var s = Of(r.Ship, thruster);
Assert.Equal(SignatureChannel.DrivePlume, s.Channel);
Assert.Same(thruster.Item, s.Item);
Assert.Equal(visibility, s.Strength, 3);
}
// The radiator's light is HeatRadiationMultiplier x Emissivity at a radiation exponent of 0 (the temperature
// drops out), so the strength is a function of two authored inputs: .25 x 3.
[Fact]
public void ARadiatorShowsAThermalSignatureWithItsItem()
{
_radiationMultiplier = .25f;
_radiationExponent = 0f;
var r = Build(Armament.None, 0f, null, new RadiatorData
{
Emissivity = Constant(3), PumpedHeat = Constant(0), TemperatureFloor = 0, WasteHeat = Constant(0), EnergyUsage = Constant(0), ThermalMass = Constant(1000)
});
var radiator = Single<Radiator>(r.Ship);
var s = Of(r.Ship, radiator);
Assert.Equal(SignatureChannel.Thermal, s.Channel);
Assert.Same(radiator.Item, s.Item);
Assert.Equal(.75f, s.Strength, 3);
}
// At a radiation exponent of 2 the strength is T^2 x multiplier x emissivity of the radiator's own temperature,
// at two different temperatures: a signature that ignores temperature cannot match both.
[Fact]
public void ARadiatorsStrengthFollowsItsTemperature()
{
_radiationMultiplier = .001f;
_radiationExponent = 2f;
var r = Build(Armament.None, 0f, null, new RadiatorData
{
Emissivity = Constant(3), PumpedHeat = Constant(0), TemperatureFloor = 0, WasteHeat = Constant(0), EnergyUsage = Constant(0), ThermalMass = Constant(1000)
});
var radiator = Single<Radiator>(r.Ship);
var seen = new System.Collections.Generic.List<double>();
for (var round = 0; round < 2; round++)
{
var t = radiator.RadiatorTemperature;
seen.Add(t);
Assert.True(t > 1f, "fixture: the radiator is warm");
radiator.Update(0f); // no cooling: the entry is the formula at exactly t
var expected = Math.Pow(t, 2) * .001 * 3;
Assert.InRange(Of(r.Ship, radiator).Strength, expected * .999, expected * 1.001);
r.Zone.Update(1f); // the radiator cools
}
Assert.True(Math.Abs(seen[0] - seen[1]) > 1e-3, "fixture: the two temperatures differ");
}
// A tick that cools the radiator publishes the strength of the temperature it started the tick at, not the
// cooled one (follow-up radiator-sampling-instant-pin).
[Fact]
public void ARadiatorsSignatureIsSampledBeforeTheTicksCooling()
{
_radiationMultiplier = .001f;
_radiationExponent = 2f;
var r = Build(Armament.None, 0f, null, new RadiatorData
{
Emissivity = Constant(3), PumpedHeat = Constant(0), TemperatureFloor = 0, WasteHeat = Constant(0), EnergyUsage = Constant(0), ThermalMass = Constant(1000)
});
var radiator = Single<Radiator>(r.Ship);
var before = radiator.RadiatorTemperature;
Assert.True(before > 1f, "fixture: the radiator is warm");
radiator.Update(1f);
var cooled = radiator.RadiatorTemperature;
Assert.True(cooled < before - 1e-3, "fixture: this tick cooled the radiator");
var expected = Math.Pow(before, 2) * .001 * 3;
Assert.InRange(Of(r.Ship, radiator).Strength, expected * .999, expected * 1.001);
}
[Fact]
public void AReflectorShowsAReflectionWithItsItem()
{
var r = Build(Armament.None, 0f, null, new ReflectorData { CrossSection = Constant(3) });
var reflector = Single<Reflector>(r.Ship);
var s = Of(r.Ship, reflector);
Assert.Equal(SignatureChannel.Reflection, s.Channel);
Assert.Same(reflector.Item, s.Item);
}
[Fact]
public void TheHullRadiatesOnTheThermalChannelWithNoItem()
{
var r = Build(Armament.None, 0f, null);
var s = Of(r.Ship, r.Ship);
Assert.Equal(SignatureChannel.Thermal, s.Channel);
Assert.Null(s.Item);
}
[Theory]
[InlineData(5f)]
[InlineData(9f)]
public void APingShowsAnEmissionWithItsItem(float pingVisibility)
{
var r = Build(Armament.None, 0f, null, new SensorData
{
Sensitivity = Constant(3f), PingBoost = Constant(.02f), PingEnergy = Constant(0f), PingVisibility = Constant(pingVisibility),
PingRange = Constant(300f), PingCooldown = Constant(4f)
});
var sensor = Single<Sensor>(r.Ship);
sensor.Ping();
var s = Of(r.Ship, sensor);
Assert.Equal(SignatureChannel.Emission, s.Channel);
Assert.Same(sensor.Item, s.Item);
Assert.Equal(pingVisibility, s.Strength, 3);
}
// A catalog-authored Visibility behaviour travels on the channel its data names, thermal unless it says more.
[Theory]
[InlineData(SignatureChannel.Thermal)]
[InlineData(SignatureChannel.DrivePlume)]
[InlineData(SignatureChannel.Emission)]
public void AVisibilityBehaviourTravelsOnItsAuthoredChannel(SignatureChannel channel)
{
var r = Build(Armament.None, 0f, null, new VisibilityData { Visibility = Constant(7), VisibilityDecay = Constant(0), Channel = channel });
var behaviour = Single<Visibility>(r.Ship);
var s = Of(r.Ship, behaviour);
Assert.Equal(channel, s.Channel);
Assert.Same(behaviour.Item, s.Item);
Assert.Equal(7f, s.Strength, 3);
}
[Fact]
public void AVisibilityBehaviourDefaultsToThermal()
{
Assert.Equal(SignatureChannel.Thermal, new VisibilityData().Channel);
}
// ---- VisibilityIsStillTheSum ----
// Every channel lit at once: a fired gun (100 on LaunchFlash), Visibility gear on Thermal (7), Reflection (4),
// DrivePlume (3) and Emission (2), a reflector under no sun (0) and the non-radiating hull (0). Visibility is the
// sum of the strengths, 116, the value the single-number build gave on this fixture; SignatureOn splits the
// same total by channel.
[Fact]
public void VisibilityIsStillTheSum()
{
var r = Build(Armament.Gun, 0f, null,
new VisibilityData { Visibility = Constant(7), VisibilityDecay = Constant(0) },
new VisibilityData { Visibility = Constant(4), VisibilityDecay = Constant(0), Channel = SignatureChannel.Reflection },
new VisibilityData { Visibility = Constant(3), VisibilityDecay = Constant(0), Channel = SignatureChannel.DrivePlume },
new VisibilityData { Visibility = Constant(2), VisibilityDecay = Constant(0), Channel = SignatureChannel.Emission },
new ReflectorData { CrossSection = Constant(3) });
Fire(r);
var ship = r.Ship;
Assert.Equal(116f, ship.Visibility, 2);
Assert.Equal(ship.VisibilitySources.Values.Sum(s => s.Strength), ship.Visibility, 3);
Assert.Equal(100f, ship.SignatureOn(SignatureChannel.LaunchFlash), 2);
Assert.Equal(7f, ship.SignatureOn(SignatureChannel.Thermal), 2);
Assert.Equal(4f, ship.SignatureOn(SignatureChannel.Reflection), 2);
Assert.Equal(2f, ship.SignatureOn(SignatureChannel.Emission), 2);
Assert.Equal(3f, ship.SignatureOn(SignatureChannel.DrivePlume), 2);
}
// ---- DecayKeepsChannelAndItem ----
[Fact]
public void DecayKeepsChannelAndItem()
{
const float lambda = .5f;
var r = Build(Armament.Gun, lambda, null);
Fire(r);
var before = Of(r.Ship, r.Weapon);
Assert.Equal(SignatureChannel.LaunchFlash, before.Channel);
r.Zone.Update(1f);
var after = Of(r.Ship, r.Weapon);
Assert.Equal(decay(before.Strength, lambda, 1f), after.Strength, 3);
Assert.True(after.Strength < before.Strength);
Assert.Equal(SignatureChannel.LaunchFlash, after.Channel);
Assert.Same(r.WeaponItem, after.Item);
for (var i = 0; i < 30; i++) r.Zone.Update(1f);
Assert.False(r.Ship.VisibilitySources.ContainsKey(r.Weapon), "a signature below 0.1 is gone");
}
// Decay rebuilds every entry from its own. A gun, a drive that has stopped burning and a sensor that has pinged
// each wrote once and are never rewritten, so after a long tick the only thing touching them is decay; each
// still names the item of the source that wrote it, and the three items differ.
[Fact]
public void DecayKeepsEachSourcesOwnItem()
{
var r = Build(Armament.Gun, .5f, Drive(10), new SensorData
{
Sensitivity = Constant(3f), PingBoost = Constant(.02f), PingEnergy = Constant(0f), PingVisibility = Constant(5f),
PingRange = Constant(300f), PingCooldown = Constant(4f)
});
var thruster = Single<Thruster>(r.Ship);
var sensor = Single<Sensor>(r.Ship);
thruster.Item.UpdatePerformance();
Burn(r, 1f);
Burn(r, 0f); // the drive stops: its entry is no longer rewritten
sensor.Ping();
Fire(r);
var sources = new Behavior[] { thruster, sensor, r.Weapon };
// What each writer authors, named here and not read back from the fixture's own ticks.
var writes = new[] { SignatureChannel.DrivePlume, SignatureChannel.Emission, SignatureChannel.LaunchFlash };
var before = sources.Select(b => Of(r.Ship, b)).ToArray();
Assert.Equal(3, before.Select(e => e.Item).Distinct().Count());
Assert.All(before, e => Assert.True(e.Strength > 1f, "fixture: each source has a live entry"));
r.Zone.Update(1f);
for (var i = 0; i < sources.Length; i++)
{
var after = Of(r.Ship, sources[i]);
Assert.True(after.Strength < before[i].Strength, "fixture: decay touched the entry");
Assert.Same(sources[i].Item, after.Item);
Assert.Equal(writes[i], after.Channel);
}
}
// The exposed view is not the private dictionary: a cast back to Dictionary cannot write around SetSignature.
[Fact]
public void TheVisibilityViewIsNotTheWritableDictionary()
{
var r = Build(Armament.None, 0f, null);
var view = r.Ship.VisibilitySources;
Assert.False(view is Dictionary<object, Signature>);
Assert.True(ReferenceEquals(view, r.Ship.VisibilitySources), "one view object per entity"); // not Assert.Same: its failure message formats the entity graph and aborts the test host
Assert.Throws<NotSupportedException>(() => ((IDictionary<object, Signature>) view)[r.Ship] = new Signature(SignatureChannel.Thermal, 1f, null));
}
// ---- APlumeKeepsTheLargerBurn ----
[Fact]
public void APlumeKeepsTheLargerBurn()
{
var r = Build(Armament.None, 0f, Drive(10));
var thruster = Single<Thruster>(r.Ship);
thruster.Item.UpdatePerformance();
Burn(r, 1f);
var full = Of(r.Ship, thruster).Strength;
Burn(r, .5f);
Assert.Equal(full, Of(r.Ship, thruster).Strength, 3);
Assert.True(full > 1f, "fixture: the full burn lit the plume");
}
[Fact]
public void APlumeRisesToALargerBurn()
{
var r = Build(Armament.None, 0f, Drive(10));
var thruster = Single<Thruster>(r.Ship);
thruster.Item.UpdatePerformance();
Burn(r, .5f);
var half = Of(r.Ship, thruster).Strength;
Burn(r, 1f);
var full = Of(r.Ship, thruster).Strength;
Assert.True(full > half && half > 0f, $"half {half}, full {full}");
}
// ---- The persisted field ----
private static MessagePackSerializerOptions Options => CultDocumentMessagePackSerialization.OptionsFor(typeof(ZonePack).Assembly);
// The channel is Key 3 of VisibilityData (union 22): the golden bytes, not a round trip against itself.
[Fact]
public void TheChannelIsKey3OfUnion22()
{
var data = new VisibilityData { Visibility = Constant(1f), VisibilityDecay = Constant(2f), Channel = SignatureChannel.Emission };
var bytes = MessagePackSerializer.Serialize<BehaviorData>(data, Options);
var reader = new MessagePackReader(bytes);
Assert.Equal(2, reader.ReadArrayHeader());
Assert.Equal(22, reader.ReadInt32());
Assert.Equal(4, reader.ReadArrayHeader());
reader.Skip(); // Group
Assert.Equal(1f, MessagePackSerializer.Deserialize<PerformanceStat>(ref reader, Options).Min);
Assert.Equal(2f, MessagePackSerializer.Deserialize<PerformanceStat>(ref reader, Options).Min);
Assert.Equal(4, reader.ReadInt32());
}
// Append-only: a record written before the field existed (three slots) reads as Thermal.
[Fact]
public void ARecordWithoutTheChannelReadsAsThermal()
{
var buffer = new System.Buffers.ArrayBufferWriter<byte>();
var writer = new MessagePackWriter(buffer);
writer.WriteArrayHeader(2);
writer.Write(22);
writer.WriteArrayHeader(3);
writer.Write(0);
writer.Flush();
MessagePackSerializer.Serialize(buffer, Constant(7f), Options);
MessagePackSerializer.Serialize(buffer, Constant(0f), Options);
var data = Assert.IsType<VisibilityData>(MessagePackSerializer.Deserialize<BehaviorData>(buffer.WrittenMemory, Options));
Assert.Equal(SignatureChannel.Thermal, data.Channel);
Assert.Equal(7f, data.Visibility.Min);
}
// Persisted ordinals: reordering the enum would silently re-label every authored record.
[Fact]
public void TheChannelOrdinalsAreFixed()
{
Assert.Equal(0, (int) SignatureChannel.Thermal);
Assert.Equal(1, (int) SignatureChannel.Reflection);
Assert.Equal(2, (int) SignatureChannel.DrivePlume);
Assert.Equal(3, (int) SignatureChannel.LaunchFlash);
Assert.Equal(4, (int) SignatureChannel.Emission);
Assert.Equal(5, Enum.GetValues(typeof(SignatureChannel)).Length);
}
[Fact]
public void TheChannelIsTheJsonPropertyChannel()
{
var json = JObject.Parse(JsonConvert.SerializeObject(new VisibilityData { Channel = SignatureChannel.Emission }));
Assert.Equal(4, (int) json["channel"]);
}
// Through the real catalog store: authored channels come back from a reopened file, the unauthored one Thermal.
[Fact]
public void AnAuthoredChannelSurvivesTheCatalogFile()
{
var path = Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc");
using (var cache = AetheriaStores.Open(path, catalogWritable: true))
{
cache.Upsert(new VerseGrammar { Revision = 1 });
cache.Upsert(Gear("Plume", new VisibilityData { Visibility = Constant(1), Channel = SignatureChannel.DrivePlume }));
cache.Upsert(Gear("Plain", new VisibilityData { Visibility = Constant(1) }));
cache.FlushAsync().Wait();
}
using var reopened = AetheriaStores.Open(path, catalogWritable: true);
VisibilityData Read(string name) => Assert.IsType<VisibilityData>(reopened.GetByName<GearData>(name).Behaviors.Single());
Assert.Equal(SignatureChannel.DrivePlume, Read("Plume").Channel);
Assert.Equal(SignatureChannel.Thermal, Read("Plain").Channel);
}
}