Shallow Live
This commit is contained in:
parent
44371365b3
commit
048b215814
7 changed files with 652 additions and 9 deletions
|
|
@ -4691,6 +4691,17 @@ public static class AgentHarness
|
|||
detail = audit.Detail;
|
||||
break;
|
||||
}
|
||||
case "event_inject_coverage":
|
||||
{
|
||||
Actor focus = ResolveUnit(null)
|
||||
?? (MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null)
|
||||
?? WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f);
|
||||
EventInjectCoverageResult coverage =
|
||||
EventInjectCoverageHarness.Run(HarnessDir(), focus);
|
||||
pass = coverage.Passed;
|
||||
detail = coverage.Detail;
|
||||
break;
|
||||
}
|
||||
case "mutation_discovery":
|
||||
{
|
||||
MutationDiscoveryResult discovery = MutationDiscoveryHarness.Run(HarnessDir());
|
||||
|
|
|
|||
533
IdleSpectator/EventInjectCoverageHarness.cs
Normal file
533
IdleSpectator/EventInjectCoverageHarness.cs
Normal file
|
|
@ -0,0 +1,533 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
public sealed class EventInjectCoverageResult
|
||||
{
|
||||
public bool Passed;
|
||||
public string Detail = "";
|
||||
public int Attempted;
|
||||
public int Registered;
|
||||
public int SkippedB;
|
||||
public int Failed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data-driven Layer A inject coverage: every camera-worthy catalog id must register
|
||||
/// an interest key. New JSON/live-seeded ids join automatically via AuthoredIds.
|
||||
/// Does not prove live Beh/API outcomes - see event_live_outcome_smoke for that track.
|
||||
///
|
||||
/// Each successful inject is removed from <see cref="InterestRegistry"/> immediately so
|
||||
/// <see cref="InterestRegistry.MaxCandidates"/> trim cannot false-fail later ids.
|
||||
/// </summary>
|
||||
public static class EventInjectCoverageHarness
|
||||
{
|
||||
public static EventInjectCoverageResult Run(string outputDirectory, Actor unit)
|
||||
{
|
||||
var result = new EventInjectCoverageResult();
|
||||
if (unit == null || !unit.isAlive())
|
||||
{
|
||||
result.Detail = "no living unit for inject coverage";
|
||||
return result;
|
||||
}
|
||||
|
||||
InterestRegistry.Clear();
|
||||
|
||||
var tsv = new StringBuilder();
|
||||
tsv.AppendLine("domain\tid\tcamera\tresult\tkey\tnotes");
|
||||
var failures = new List<string>(32);
|
||||
|
||||
void Row(string domain, string id, bool camera, string outcome, string key, string notes)
|
||||
{
|
||||
tsv.Append(domain).Append('\t')
|
||||
.Append(id).Append('\t')
|
||||
.Append(camera ? "true" : "false").Append('\t')
|
||||
.Append(outcome).Append('\t')
|
||||
.Append(key ?? "").Append('\t')
|
||||
.Append(notes ?? "").Append('\n');
|
||||
}
|
||||
|
||||
void Fail(string domain, string id, string notes)
|
||||
{
|
||||
result.Failed++;
|
||||
failures.Add(domain + ":" + id + " " + notes);
|
||||
Row(domain, id, true, "FAIL", "", notes);
|
||||
}
|
||||
|
||||
void SkipB(string domain, string id, string notes)
|
||||
{
|
||||
result.SkippedB++;
|
||||
Row(domain, id, false, "skip_b", "", notes);
|
||||
}
|
||||
|
||||
void Ok(string domain, string id, string key)
|
||||
{
|
||||
result.Registered++;
|
||||
Row(domain, id, true, "ok", key, "");
|
||||
}
|
||||
|
||||
bool Accept(InterestCandidate c, string needle, out string key, out string notes)
|
||||
{
|
||||
key = "";
|
||||
notes = "";
|
||||
if (c == null || string.IsNullOrEmpty(c.Key))
|
||||
{
|
||||
notes = "register_returned_null";
|
||||
return false;
|
||||
}
|
||||
|
||||
key = c.Key;
|
||||
if (!string.IsNullOrEmpty(needle)
|
||||
&& c.Key.IndexOf(needle, StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
notes = "key_mismatch want=" + needle + " got=" + c.Key;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Drop immediately so MaxCandidates trim cannot wipe later injects.
|
||||
InterestRegistry.Remove(c.Key);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Happiness Signal
|
||||
foreach (string id in EventCatalog.Happiness.AuthoredIds)
|
||||
{
|
||||
result.Attempted++;
|
||||
HappinessCatalogEntry entry = EventCatalog.Happiness.GetOrFallback(id);
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
SkipB("happiness", id, entry.Presentation.ToString());
|
||||
continue;
|
||||
}
|
||||
|
||||
string label = EventReason.Apply("{a} · " + id, unit, id: id);
|
||||
InterestCandidate c = InterestFeeds.RegisterHappinessSignal(unit, id, label);
|
||||
string needle = "happiness:" + id;
|
||||
if (!Accept(c, needle, out string key, out string notes))
|
||||
{
|
||||
Fail("happiness", id, notes);
|
||||
continue;
|
||||
}
|
||||
|
||||
Ok("happiness", id, key);
|
||||
}
|
||||
|
||||
// Status CreatesInterest (gains)
|
||||
foreach (string id in EventCatalog.Status.AuthoredIds)
|
||||
{
|
||||
result.Attempted++;
|
||||
StatusInterestEntry entry = EventCatalog.Status.GetOrFallback(id);
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
SkipB("status", id, "CreatesInterest=false");
|
||||
continue;
|
||||
}
|
||||
|
||||
string phrase = ActivityStatusProse.Phrase(id, gained: true, out _);
|
||||
string key = "status:" + id + ":" + EventFeedUtil.SafeId(unit);
|
||||
InterestCandidate c = EventFeedUtil.Register(
|
||||
key,
|
||||
"status_inject",
|
||||
string.IsNullOrEmpty(entry.Category) ? "StatusTransformation" : entry.Category,
|
||||
EventReason.Status(unit, phrase),
|
||||
id,
|
||||
entry.EventStrength,
|
||||
unit.current_position,
|
||||
unit);
|
||||
if (!Accept(c, "status:" + id, out string accepted, out string notes))
|
||||
{
|
||||
Fail("status", id, notes);
|
||||
continue;
|
||||
}
|
||||
|
||||
Ok("status", id, accepted);
|
||||
}
|
||||
|
||||
// WorldLog
|
||||
foreach (string id in EventCatalog.WorldLog.AuthoredIds)
|
||||
{
|
||||
result.Attempted++;
|
||||
WorldLogEventEntry entry = EventCatalog.WorldLog.GetOrFallback(id);
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
SkipB("worldLog", id, "CreatesInterest=false");
|
||||
continue;
|
||||
}
|
||||
|
||||
string key = "worldlog:" + id + ":inject";
|
||||
string label = entry.MakeLabel("Alpha", "Beta");
|
||||
InterestCandidate c = EventFeedUtil.Register(
|
||||
key,
|
||||
"worldlog_inject",
|
||||
string.IsNullOrEmpty(entry.Category) ? "WorldLog" : entry.Category,
|
||||
label,
|
||||
id,
|
||||
entry.EventStrength,
|
||||
unit.current_position,
|
||||
unit,
|
||||
locationOnly: false);
|
||||
if (!Accept(c, "worldlog:" + id, out string accepted, out string notes))
|
||||
{
|
||||
Fail("worldLog", id, notes);
|
||||
continue;
|
||||
}
|
||||
|
||||
Ok("worldLog", id, accepted);
|
||||
}
|
||||
|
||||
// Disasters → paired WorldLog id (policy overlay; camera via worldLog)
|
||||
foreach (string id in EventCatalog.Disaster.AuthoredIds)
|
||||
{
|
||||
result.Attempted++;
|
||||
DisasterInterestEntry entry = EventCatalog.Disaster.GetOrFallback(id);
|
||||
if (entry == null || !EventCatalog.IsCameraWorthy(entry.CreatesInterest))
|
||||
{
|
||||
SkipB("disasters", id, "camera=false");
|
||||
continue;
|
||||
}
|
||||
|
||||
string worldLogId = string.IsNullOrEmpty(entry.WorldLogId)
|
||||
? ("disaster_" + id)
|
||||
: entry.WorldLogId;
|
||||
WorldLogEventEntry wl = EventCatalog.WorldLog.GetOrFallback(worldLogId);
|
||||
if (!EventCatalog.IsCameraWorthy(wl))
|
||||
{
|
||||
Fail("disasters", id, "paired_worldLog_not_camera:" + worldLogId);
|
||||
continue;
|
||||
}
|
||||
|
||||
string key = "worldlog:" + worldLogId + ":disaster_inject";
|
||||
InterestCandidate c = EventFeedUtil.Register(
|
||||
key,
|
||||
"disaster_inject",
|
||||
"Disaster",
|
||||
wl.MakeLabel("Alpha", "Beta"),
|
||||
worldLogId,
|
||||
entry.EventStrength,
|
||||
unit.current_position,
|
||||
unit);
|
||||
if (!Accept(c, "worldlog:" + worldLogId, out string accepted, out string notes))
|
||||
{
|
||||
Fail("disasters", id, notes + " worldLog=" + worldLogId);
|
||||
continue;
|
||||
}
|
||||
|
||||
Ok("disasters", id, accepted);
|
||||
}
|
||||
|
||||
// War types
|
||||
foreach (string id in EventCatalog.WarType.AuthoredIds)
|
||||
{
|
||||
result.Attempted++;
|
||||
WarTypeInterestEntry entry = EventCatalog.WarType.GetOrFallback(id);
|
||||
if (entry == null || !EventCatalog.IsCameraWorthy(entry.CreatesInterest))
|
||||
{
|
||||
SkipB("warTypes", id, "camera=false");
|
||||
continue;
|
||||
}
|
||||
|
||||
string key = "war:inject:" + id;
|
||||
string label = EventReason.Apply(entry.LabelTemplate, unit, id: id);
|
||||
if (string.IsNullOrEmpty(label))
|
||||
{
|
||||
label = "War (" + id + ")";
|
||||
}
|
||||
|
||||
InterestCandidate c = EventFeedUtil.Register(
|
||||
key,
|
||||
"war_inject",
|
||||
string.IsNullOrEmpty(entry.Category) ? "War" : entry.Category,
|
||||
label,
|
||||
id,
|
||||
entry.EventStrength,
|
||||
unit.current_position,
|
||||
unit);
|
||||
if (!Accept(c, key, out string accepted, out string notes))
|
||||
{
|
||||
Fail("warTypes", id, notes);
|
||||
continue;
|
||||
}
|
||||
|
||||
Ok("warTypes", id, accepted);
|
||||
}
|
||||
|
||||
InjectDiscreteDomain(
|
||||
"relationship",
|
||||
EventCatalog.Relationship.AuthoredIds,
|
||||
id => EventCatalog.Relationship.GetOrFallback(id),
|
||||
"rel",
|
||||
unit,
|
||||
result,
|
||||
Fail,
|
||||
SkipB,
|
||||
Ok,
|
||||
Accept);
|
||||
|
||||
InjectDiscreteDomain(
|
||||
"plots",
|
||||
EventCatalog.Plot.AuthoredIds,
|
||||
id => EventCatalog.Plot.GetOrFallback(id),
|
||||
"plot",
|
||||
unit,
|
||||
result,
|
||||
Fail,
|
||||
SkipB,
|
||||
Ok,
|
||||
Accept,
|
||||
phaseAware: true);
|
||||
|
||||
InjectDiscreteDomain(
|
||||
"books",
|
||||
EventCatalog.Book.AuthoredIds,
|
||||
id => EventCatalog.Book.GetOrFallback(id),
|
||||
"book",
|
||||
unit,
|
||||
result,
|
||||
Fail,
|
||||
SkipB,
|
||||
Ok,
|
||||
Accept);
|
||||
|
||||
InjectDiscreteDomain(
|
||||
"eras",
|
||||
EventCatalog.Era.AuthoredIds,
|
||||
id => EventCatalog.Era.GetOrFallback(id),
|
||||
"era",
|
||||
unit,
|
||||
result,
|
||||
Fail,
|
||||
SkipB,
|
||||
Ok,
|
||||
Accept,
|
||||
locationOnlyOk: true);
|
||||
|
||||
InjectDiscreteDomain(
|
||||
"traits",
|
||||
EventCatalog.Trait.AuthoredIds,
|
||||
id => EventCatalog.Trait.GetOrFallback(id),
|
||||
"trait",
|
||||
unit,
|
||||
result,
|
||||
Fail,
|
||||
SkipB,
|
||||
Ok,
|
||||
Accept,
|
||||
useTraitLabel: true);
|
||||
|
||||
// Decisions: only camera-worthy Signal decisions (live-seeded + JSON)
|
||||
foreach (string id in EventCatalog.Decision.AuthoredIds)
|
||||
{
|
||||
result.Attempted++;
|
||||
if (!EventCatalog.Decision.IsCameraWorthy(id))
|
||||
{
|
||||
SkipB("decisions", id, "not_interesting_decision");
|
||||
continue;
|
||||
}
|
||||
|
||||
DiscreteEventEntry entry = EventCatalog.Decision.GetOrFallback(id);
|
||||
string key = "decision:" + id + ":" + EventFeedUtil.SafeId(unit);
|
||||
string label = EventReason.Decision(unit, id);
|
||||
InterestCandidate c = EventFeedUtil.Register(
|
||||
key,
|
||||
"decision_inject",
|
||||
entry.Category,
|
||||
label,
|
||||
id,
|
||||
entry.EventStrength,
|
||||
unit.current_position,
|
||||
unit);
|
||||
if (!Accept(c, "decision:" + id, out string accepted, out string notes))
|
||||
{
|
||||
Fail("decisions", id, notes);
|
||||
continue;
|
||||
}
|
||||
|
||||
Ok("decisions", id, accepted);
|
||||
}
|
||||
|
||||
// Libraries - camera-worthy only (Signal / ambientCreatesInterest policy)
|
||||
InjectLibrary("spell", EventCatalog.Spell.AuthoredIds, id => EventCatalog.Spell.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
|
||||
InjectLibrary("item", EventCatalog.Item.AuthoredIds, id => EventCatalog.Item.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
|
||||
InjectLibrary("power", EventCatalog.Power.AuthoredIds, id => EventCatalog.Power.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
|
||||
InjectLibrary("gene", EventCatalog.Gene.AuthoredIds, id => EventCatalog.Gene.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
|
||||
InjectLibrary("phenotype", EventCatalog.Phenotype.AuthoredIds, id => EventCatalog.Phenotype.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
|
||||
InjectLibrary("worldLaw", EventCatalog.WorldLaw.AuthoredIds, id => EventCatalog.WorldLaw.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
|
||||
InjectLibrary("subspecies_trait", EventCatalog.SubspeciesTrait.AuthoredIds, id => EventCatalog.SubspeciesTrait.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
|
||||
InjectLibrary("biome", EventCatalog.Biome.AuthoredIds, id => EventCatalog.Biome.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
|
||||
|
||||
CatalogCoverageAudit.WriteReport(outputDirectory, "event-inject-coverage.tsv", tsv.ToString());
|
||||
|
||||
result.Passed = result.Failed == 0 && result.Registered > 0;
|
||||
result.Detail =
|
||||
$"attempted={result.Attempted} registered={result.Registered} skip_b={result.SkippedB} "
|
||||
+ $"failed={result.Failed}"
|
||||
+ (failures.Count > 0
|
||||
? (" first=" + failures[0] + (failures.Count > 1 ? " +" + (failures.Count - 1) : ""))
|
||||
: "");
|
||||
return result;
|
||||
}
|
||||
|
||||
private delegate DiscreteEventEntry DiscreteGetter(string id);
|
||||
|
||||
private delegate bool AcceptFn(InterestCandidate c, string needle, out string key, out string notes);
|
||||
|
||||
private static void InjectDiscreteDomain(
|
||||
string domain,
|
||||
IEnumerable<string> ids,
|
||||
DiscreteGetter get,
|
||||
string keyPrefix,
|
||||
Actor unit,
|
||||
EventInjectCoverageResult result,
|
||||
Action<string, string, string> fail,
|
||||
Action<string, string, string> skipB,
|
||||
Action<string, string, string> ok,
|
||||
AcceptFn accept,
|
||||
bool phaseAware = false,
|
||||
bool locationOnlyOk = false,
|
||||
bool useTraitLabel = false)
|
||||
{
|
||||
if (ids == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (string raw in ids)
|
||||
{
|
||||
result.Attempted++;
|
||||
string id = (raw ?? "").Trim();
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DiscreteEventEntry entry = get(id);
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
skipB(domain, id, "CreatesInterest=false");
|
||||
continue;
|
||||
}
|
||||
|
||||
string key = keyPrefix + ":" + id
|
||||
+ (phaseAware ? ":new:" : ":")
|
||||
+ (locationOnlyOk ? "inject" : EventFeedUtil.SafeId(unit).ToString());
|
||||
if (domain == "eras")
|
||||
{
|
||||
key = "era:" + id;
|
||||
}
|
||||
else if (domain == "traits")
|
||||
{
|
||||
key = "trait:" + id + ":gains:" + EventFeedUtil.SafeId(unit);
|
||||
}
|
||||
else if (domain == "relationship")
|
||||
{
|
||||
key = "rel:" + id + ":" + EventFeedUtil.SafeId(unit);
|
||||
}
|
||||
else if (domain == "plots")
|
||||
{
|
||||
key = "plot:" + id + ":new:" + EventFeedUtil.SafeId(unit);
|
||||
}
|
||||
else if (domain == "books")
|
||||
{
|
||||
key = "book:" + id + ":new:" + EventFeedUtil.SafeId(unit);
|
||||
}
|
||||
|
||||
string label;
|
||||
if (useTraitLabel)
|
||||
{
|
||||
label = EventReason.Trait(unit, id, gained: true);
|
||||
}
|
||||
else if (phaseAware && domain == "plots")
|
||||
{
|
||||
label = EventReason.Plot(unit, entry.LabelTemplate, "new", entry.Id);
|
||||
}
|
||||
else if (domain == "eras")
|
||||
{
|
||||
label = entry.MakeLabel("", "");
|
||||
}
|
||||
else
|
||||
{
|
||||
label = entry.MakeLabel(unit);
|
||||
}
|
||||
|
||||
InterestCandidate c = EventFeedUtil.Register(
|
||||
key,
|
||||
domain + "_inject",
|
||||
entry.Category,
|
||||
label,
|
||||
id,
|
||||
entry.EventStrength,
|
||||
unit.current_position,
|
||||
follow: unit,
|
||||
locationOnly: false);
|
||||
string needle = keyPrefix == "rel" ? "rel:" + id : key.Split(':')[0] + ":" + id;
|
||||
if (domain == "eras")
|
||||
{
|
||||
needle = "era:" + id;
|
||||
}
|
||||
|
||||
if (!accept(c, needle, out string accepted, out string notes))
|
||||
{
|
||||
fail(domain, id, notes);
|
||||
continue;
|
||||
}
|
||||
|
||||
ok(domain, id, accepted);
|
||||
}
|
||||
}
|
||||
|
||||
private static void InjectLibrary(
|
||||
string domain,
|
||||
IEnumerable<string> ids,
|
||||
DiscreteGetter get,
|
||||
Actor unit,
|
||||
EventInjectCoverageResult result,
|
||||
Action<string, string, string> fail,
|
||||
Action<string, string, string> skipB,
|
||||
Action<string, string, string> ok,
|
||||
AcceptFn accept)
|
||||
{
|
||||
if (ids == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (string raw in ids)
|
||||
{
|
||||
result.Attempted++;
|
||||
string id = (raw ?? "").Trim();
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DiscreteEventEntry entry = get(id);
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
skipB(domain, id, "CreatesInterest=false");
|
||||
continue;
|
||||
}
|
||||
|
||||
string key = domain + ":" + id + ":" + EventFeedUtil.SafeId(unit);
|
||||
string label = EventReason.Library(unit, domain, id);
|
||||
InterestCandidate c = EventFeedUtil.Register(
|
||||
key,
|
||||
domain + "_inject",
|
||||
entry.Category,
|
||||
label,
|
||||
id,
|
||||
entry.EventStrength,
|
||||
unit.current_position,
|
||||
unit);
|
||||
if (!accept(c, domain + ":" + id, out string accepted, out string notes))
|
||||
{
|
||||
fail(domain, id, notes);
|
||||
continue;
|
||||
}
|
||||
|
||||
ok(domain, id, accepted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,6 +58,12 @@ internal static class HarnessScenarios
|
|||
return DisasterWarAudit();
|
||||
case "event_coverage":
|
||||
return EventCoverage();
|
||||
case "event_inject_coverage":
|
||||
return EventInjectCoverage();
|
||||
case "event_live_outcome_smoke":
|
||||
return EventLiveOutcomeSmoke();
|
||||
case "event_suite":
|
||||
return EventSuite();
|
||||
case "domain_event_audit":
|
||||
return DomainEventAudit();
|
||||
case "event_tracking":
|
||||
|
|
@ -164,7 +170,7 @@ internal static class HarnessScenarios
|
|||
Nested("reg_action_priority", "director_action_priority"),
|
||||
Nested("reg_discovery", "discovery"),
|
||||
Nested("reg_worldlog", "world_log"),
|
||||
Nested("reg_event_coverage", "event_coverage"),
|
||||
Nested("reg_event_coverage", "event_suite"),
|
||||
Nested("reg_chronicle", "chronicle_smoke"),
|
||||
Nested("reg_activity", "activity_idle_smoke"),
|
||||
Nested("reg_activity_status", "activity_status_smoke"),
|
||||
|
|
@ -1303,11 +1309,8 @@ internal static class HarnessScenarios
|
|||
Step("ec13", "assert", expect: "activity_happiness_audit"),
|
||||
Step("ec14", "assert", expect: "domain_event_audit"),
|
||||
|
||||
// Previously silent-dropped WorldLog disaster id must enter the registry
|
||||
Step("ec20", "world_log_feed", asset: "disaster_tornado"),
|
||||
Step("ec21", "assert", expect: "interest_has_key", value: "worldlog:disaster_tornado"),
|
||||
Step("ec22", "world_log_feed", asset: "disaster_alien_invasion"),
|
||||
Step("ec23", "assert", expect: "interest_has_key", value: "worldlog:disaster_alien_invasion"),
|
||||
// Every camera-worthy catalog id must A-register (data-driven).
|
||||
Step("ec30", "assert", expect: "event_inject_coverage"),
|
||||
|
||||
Step("ec90", "assert", expect: "health"),
|
||||
Step("ec91", "assert", expect: "no_bad"),
|
||||
|
|
@ -1315,6 +1318,60 @@ internal static class HarnessScenarios
|
|||
};
|
||||
}
|
||||
|
||||
private static List<HarnessCommand> EventInjectCoverage()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
{
|
||||
Step("eic0", "dismiss_windows"),
|
||||
Step("eic1", "wait_world"),
|
||||
Step("eic2", "set_setting", expect: "enabled", value: "true"),
|
||||
Step("eic3", "pick_unit", asset: "auto"),
|
||||
Step("eic4", "spectator", value: "off"),
|
||||
Step("eic5", "spectator", value: "on"),
|
||||
Step("eic6", "focus", asset: "auto"),
|
||||
Step("eic7", "assert", expect: "event_inject_coverage"),
|
||||
Step("eic8", "assert", expect: "no_bad"),
|
||||
Step("eic9", "snapshot")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Curated live API/Beh outcomes (not full catalog). Expands when patches gain
|
||||
/// deterministic harness wrappers. Inject coverage owns "every id registers."
|
||||
/// </summary>
|
||||
private static List<HarnessCommand> EventLiveOutcomeSmoke()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
{
|
||||
Nested("elo_family", "family_trigger_smoke"),
|
||||
Step("elo0", "dismiss_windows"),
|
||||
Step("elo1", "wait_world"),
|
||||
Step("elo2", "pick_unit", asset: "human"),
|
||||
Step("elo3", "spectator", value: "off"),
|
||||
Step("elo4", "spectator", value: "on"),
|
||||
Step("elo5", "focus", asset: "auto"),
|
||||
Step("elo10", "activity_status_reset"),
|
||||
Step("elo11", "status_apply", value: "confused"),
|
||||
Step("elo12", "assert", expect: "activity_status_gain_count", value: "1", label: "min"),
|
||||
Step("elo13", "assert", expect: "activity_log_contains", value: "confused"),
|
||||
Step("elo19", "happiness_reset"),
|
||||
Step("elo20", "happiness_apply", value: "just_became_adult"),
|
||||
Step("elo21", "assert", expect: "activity_happiness_count", value: "1", label: "min"),
|
||||
Step("elo22", "assert", expect: "activity_log_contains", value: "adult"),
|
||||
Step("elo90", "assert", expect: "no_bad"),
|
||||
Step("elo99", "snapshot")
|
||||
};
|
||||
}
|
||||
|
||||
private static List<HarnessCommand> EventSuite()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
{
|
||||
Nested("es_coverage", "event_coverage"),
|
||||
Nested("es_live", "event_live_outcome_smoke"),
|
||||
};
|
||||
}
|
||||
|
||||
private static List<HarnessCommand> MutationDiscovery()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.25.14",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). EventOutcome observe→confirm→emit.",
|
||||
"version": "0.25.17",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Catalog-wide event inject E2E suite.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
41
docs/event-e2e.md
Normal file
41
docs/event-e2e.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Event E2E suite (maintainable)
|
||||
|
||||
Goal: every catalog event is covered by automation that stays cheap when new ids are added.
|
||||
|
||||
## Tracks
|
||||
|
||||
| Track | Scenario | What it proves | How new events join |
|
||||
|-------|----------|----------------|---------------------|
|
||||
| **Inventory audits** | nested in `event_coverage` | Live library vs authored catalog | Add JSON row / live seed; audit fails until matched |
|
||||
| **A inject coverage** | `event_inject_coverage` / `ec30` | Every **camera-worthy** id can register an interest key + label | Auto via `EventCatalog.*.AuthoredIds` |
|
||||
| **Live outcome** | `event_live_outcome_smoke` | Real API/Beh + `EventOutcome` confirm (curated) | Add a deterministic harness step when a patch exists |
|
||||
|
||||
Full suite: `event_suite` = `event_coverage` + `event_live_outcome_smoke`.
|
||||
Regression nests `event_suite`.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
./scripts/harness-run.sh event_inject_coverage
|
||||
./scripts/harness-run.sh event_live_outcome_smoke
|
||||
./scripts/harness-run.sh event_suite
|
||||
./scripts/harness-run.sh regression
|
||||
```
|
||||
|
||||
Artifact: `IdleSpectator/.harness/event-inject-coverage.tsv`
|
||||
(`domain`, `id`, `camera`, `result`, `key`, `notes`).
|
||||
|
||||
## Design rules
|
||||
|
||||
1. **Do not** hand-write one harness step per catalog id for inject - loop `AuthoredIds`.
|
||||
2. B-only rows (`CreatesInterest=false`, Ambient happiness) are `skip_b`, not failures.
|
||||
3. Inject coverage ≠ live outcome. Inject proves wiring; live proves confirm predicates.
|
||||
4. Expand live outcome slowly (family, status apply, happiness apply, …) as wrappers exist.
|
||||
5. Libraries: only camera-worthy live-seeded ids are injected (can be large; Signal-gated).
|
||||
6. After each successful inject, remove the key from `InterestRegistry`. The registry caps at 96 candidates; leaving them in causes false `register_failed` on later ids.
|
||||
|
||||
## Code
|
||||
|
||||
- `EventInjectCoverageHarness.Run` - data-driven register loop
|
||||
- Assert expect `event_inject_coverage` in `AgentHarness`
|
||||
- Observe→confirm docs: `event-observation.md`
|
||||
|
|
@ -82,6 +82,7 @@ Active EventLed A scenes should not silently blank a valid Label.
|
|||
|
||||
## See also
|
||||
|
||||
- [`event-e2e.md`](event-e2e.md) - catalog inject + live outcome suite
|
||||
- [`event-observation.md`](event-observation.md) - observe → confirm → emit (Beh Continue is not success)
|
||||
- [`event-audit.md`](event-audit.md) - sources, MaxWatch, A demotion set
|
||||
- [`event-prose-audit.md`](event-prose-audit.md) - prose vs game call sites
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ REGRESSION_SCENARIOS=(
|
|||
director_action_priority
|
||||
discovery
|
||||
world_log
|
||||
event_coverage
|
||||
event_suite
|
||||
chronicle_smoke
|
||||
activity_idle_smoke
|
||||
activity_status_smoke
|
||||
|
|
|
|||
Loading…
Reference in a new issue