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
12 changes: 12 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ Reliability rule for runtime/mod tasks:
evidence: manual `2026-02-28` `pwsh ./tools/lua-harness/run-lua-harness.ps1 -Strict`
evidence: bundle `TestResults/runs/20260228-171028/repro-bundle.json` (`classification=blocked_environment`)
evidence: bundle `TestResults/runs/20260228-171159/repro-bundle.json` (`classification=blocked_environment`)
- [x] Functional closure wave: deterministic native host bootstrap, promoted `set_unit_cap` enable->disable matrix semantics, helper forced-profile fallback diagnostics, and Codex-launched live process matrix (EAW + SWFOC tactical + AOTR + ROE) with strict bundle validation.
evidence: test `tests/SwfocTrainer.Tests/Runtime/ProcessLocatorForcedContextTests.cs`
evidence: test `tests/SwfocTrainer.Tests/Profiles/LivePromotedActionMatrixTests.cs`
evidence: test `tests/SwfocTrainer.Tests/Profiles/LiveHeroHelperWorkflowTests.cs`
evidence: manual `2026-02-28` `dotnet build SwfocTrainer.sln -c Release --no-restore` => `Build succeeded`
evidence: manual `2026-02-28` `dotnet test tests/SwfocTrainer.Tests/SwfocTrainer.Tests.csproj -c Release --no-build --filter "FullyQualifiedName!~SwfocTrainer.Tests.Profiles.Live&FullyQualifiedName!~RuntimeAttachSmokeTests"` => `Passed: 192`
evidence: manual `2026-02-28` `powershell.exe -File tools/validate-workshop-topmods.ps1 -Path tools/fixtures/workshop_topmods_sample.json -Strict` => `validation passed`
evidence: manual `2026-02-28` `powershell.exe -File tools/validate-generated-profile-seed.ps1 -Path tools/fixtures/generated_profile_seeds_sample.json -Strict` => `validation passed`
evidence: manual `2026-02-28` Session A EAW snapshot `TestResults/runs/LIVE-EAW-20260228-204540/eaw-process-snapshot.json`
evidence: bundle `TestResults/runs/LIVE-TACTICAL-20260228-211256/repro-bundle.json`
evidence: bundle `TestResults/runs/LIVE-AOTR-20260228-211521/repro-bundle.json`
evidence: bundle `TestResults/runs/LIVE-ROE-20260228-211757/repro-bundle.json`

## Later (M2 + M3 + M4)

Expand Down
17 changes: 13 additions & 4 deletions docs/LIVE_VALIDATION_RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ Use this runbook to gather real-machine evidence for runtime/mod issues and mile
## 1. Preconditions

- Launch the target game session first (`swfoc.exe` / `StarWarsG.exe`).
- Ensure extender bridge host is running for extender-routed credits checks:
- `SwfocExtender.Host` on pipe `SwfocExtenderBridge`.
- `tools/run-live-validation.ps1` preflights native host build and wires:
- `native/runtime/SwfocExtender.Host.exe`
- `SWFOC_EXTENDER_HOST_PATH` for every `dotnet test` subprocess.
- If running a live test command manually (outside run-live script), set `SWFOC_EXTENDER_HOST_PATH` explicitly to avoid nondeterministic host resolution.
- Promoted actions are extender-authoritative and fail-closed:
- no managed/memory fallback is accepted for promoted matrix evidence.
- missing or unverified promoted capability must surface fail-closed diagnostics.
Expand Down Expand Up @@ -166,6 +168,13 @@ Expected evidence behavior for promoted actions:
- `hasFallbackMarker=false`
- fail-closed outcomes use explicit route diagnostics (`SAFETY_FAIL_CLOSED`) and block issue `#7` closure.

`set_unit_cap` promoted matrix contract:

- run as a deterministic two-step sequence per profile:
1. enable (`enable=true`, snapshot/capture)
2. disable (`enable=false`, restore)
- disable-first behavior is intentionally fail-closed and must not be asserted as pass in matrix evidence.

## 5. Bundle Validation

```powershell
Expand Down Expand Up @@ -202,8 +211,8 @@ Close issues only when all required evidence is present:
Issue `#7` evidence decision gate:

- `actionStatusDiagnostics.status` is `captured`.
- `actionStatusDiagnostics.summary.total=15`, `passed=15`, `failed=0`, `skipped=0`.
- every matrix entry for the five promoted actions across `base_swfoc`, `aotr_1397421866_swfoc`, and `roe_3447786229_swfoc` is present.
- `actionStatusDiagnostics.summary.total=18`, `passed=18`, `failed=0`, `skipped=0` for full-context closure runs.
- every matrix entry for the promoted actions across `base_swfoc`, `aotr_1397421866_swfoc`, and `roe_3447786229_swfoc` is present, including `set_unit_cap[1/2]` and `set_unit_cap[2/2]` per profile.
- no matrix entry includes fallback markers or blocked diagnostics (`hasFallbackMarker=true`, `backendRoute` not `Extender`, or non-pass route/probe reason codes).
- top-mod rows (when present) must never be silent omissions; each row must be explicit `Passed`, `Failed`, or `Skipped` with `skipReasonCode` when skipped.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ namespace SwfocTrainer.Tests.Profiles;

public sealed class LiveHeroHelperWorkflowTests
{
private static readonly HashSet<string> ForcedHelperProfileIds = new(StringComparer.OrdinalIgnoreCase)
{
"aotr_1397421866_swfoc",
"roe_3447786229_swfoc"
};

private readonly ITestOutputHelper _output;

public LiveHeroHelperWorkflowTests(ITestOutputHelper output)
Expand Down Expand Up @@ -40,6 +46,7 @@ public async Task Hero_Helper_Workflow_Should_Return_Action_Result_For_Aotr_Or_R

var profiles = await ResolveProfilesAsync(profileRepo);
var target = SelectHelperTargetContext(supported, profiles);
WriteHelperContextDiagnostics(supported, target);

if (target is null)
{
Expand Down Expand Up @@ -114,12 +121,86 @@ private static void AssertHelperActionResult(ActionExecutionResult result)
x.LaunchContext ?? new LaunchContextResolver().Resolve(x, profiles)))
.ToList();

return contexts
var selected = contexts
.OrderByDescending(x => x.Context.Recommendation.ProfileId == "roe_3447786229_swfoc")
.ThenByDescending(x => x.Context.Recommendation.ProfileId == "aotr_1397421866_swfoc")
.FirstOrDefault(x =>
x.Context.Recommendation.ProfileId == "roe_3447786229_swfoc" ||
x.Context.Recommendation.ProfileId == "aotr_1397421866_swfoc");
if (selected is not null)
{
return selected;
}

var forcedProfileId = NormalizeProfileId(Environment.GetEnvironmentVariable("SWFOC_FORCE_PROFILE_ID"));
if (!IsSupportedForcedHelperProfile(forcedProfileId))
{
return null;
}

var forcedTarget = contexts
.OrderByDescending(x => x.Context.Source.Equals("forced", StringComparison.OrdinalIgnoreCase))
.ThenByDescending(x => x.Process.ProcessName.Equals("StarWarsG", StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();
if (forcedTarget is null)
{
return null;
}

var forcedContext = forcedTarget.Context with
{
Recommendation = new ProfileRecommendation(
forcedProfileId,
ReasonCode: "forced_profile_env_fallback",
Confidence: 1.0),
Source = "forced"
};
return forcedTarget with { Context = forcedContext };
}

private void WriteHelperContextDiagnostics(
IReadOnlyList<ProcessMetadata> supported,
SupportedProcessContext? selected)
{
var forcedProfileId = NormalizeProfileId(Environment.GetEnvironmentVariable("SWFOC_FORCE_PROFILE_ID"));
var forcedWorkshopIds = Environment.GetEnvironmentVariable("SWFOC_FORCE_WORKSHOP_IDS") ?? string.Empty;
_output.WriteLine($"helper forced profile env={forcedProfileId ?? "<empty>"}");
_output.WriteLine($"helper forced workshop env={forcedWorkshopIds}");

foreach (var process in supported.Where(x => x.ExeTarget == ExeTarget.Swfoc))
{
var launchContext = process.LaunchContext;
var recommendation = launchContext?.Recommendation;
var steamIds = launchContext is null
? "<none>"
: string.Join(",", launchContext.SteamModIds);
_output.WriteLine(
$"helper candidate pid={process.ProcessId} name={process.ProcessName} path={process.ProcessPath} source={launchContext?.Source ?? "<none>"} profile={recommendation?.ProfileId ?? "<none>"} reason={recommendation?.ReasonCode ?? "<none>"} steammods={steamIds}");
}

if (selected is null)
{
_output.WriteLine("helper selection: <none>");
return;
}

_output.WriteLine(
$"helper selected pid={selected.Process.ProcessId} profile={selected.Context.Recommendation.ProfileId} reason={selected.Context.Recommendation.ReasonCode} source={selected.Context.Source}");
}

private static bool IsSupportedForcedHelperProfile(string? profileId)
{
if (string.IsNullOrWhiteSpace(profileId))
{
return false;
}

return ForcedHelperProfileIds.Contains(profileId);
}

private static string? NormalizeProfileId(string? profileId)
{
return string.IsNullOrWhiteSpace(profileId) ? null : profileId.Trim();
}

private static async Task<IReadOnlyList<TrainerProfile>> ResolveProfilesAsync(FileSystemProfileRepository profileRepo)
Expand Down
145 changes: 88 additions & 57 deletions tests/SwfocTrainer.Tests/Profiles/LivePromotedActionMatrixTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,38 +28,58 @@ public sealed class LivePromotedActionMatrixTests
[
new(
"freeze_timer",
() => new JsonObject
{
["symbol"] = "game_timer_freeze",
["boolValue"] = false
}),
() =>
[
new JsonObject
{
["symbol"] = "game_timer_freeze",
["boolValue"] = false
}
]),
new(
"toggle_fog_reveal",
() => new JsonObject
{
["symbol"] = "fog_reveal",
["boolValue"] = false
}),
() =>
[
new JsonObject
{
["symbol"] = "fog_reveal",
["boolValue"] = false
}
]),
new(
"toggle_ai",
() => new JsonObject
{
["symbol"] = "ai_enabled",
["boolValue"] = true
}),
() =>
[
new JsonObject
{
["symbol"] = "ai_enabled",
["boolValue"] = true
}
]),
new(
"set_unit_cap",
() => new JsonObject
{
["intValue"] = 300,
["enable"] = false
}),
() =>
[
new JsonObject
{
["intValue"] = 300,
["enable"] = true
},
new JsonObject
{
["intValue"] = 300,
["enable"] = false
}
]),
new(
"toggle_instant_build_patch",
() => new JsonObject
{
["enable"] = false
})
() =>
[
new JsonObject
{
["enable"] = false
}
])
];

private readonly ITestOutputHelper _output;
Expand Down Expand Up @@ -197,38 +217,49 @@ private async Task ExecuteAndAssertActionAsync(
because: $"profile '{profileId}' must expose promoted action '{actionSpec.ActionId}'");

var action = profile.Actions[actionSpec.ActionId];
var request = new ActionExecutionRequest(action, actionSpec.BuildPayload(), profileId, mode);
var result = await runtime.ExecuteAsync(request);
var backendRoute = ReadDiagnosticString(result.Diagnostics, "backendRoute");
var routeReasonCode = ReadDiagnosticString(result.Diagnostics, "routeReasonCode");
var capabilityProbeReasonCode = ReadDiagnosticString(result.Diagnostics, "capabilityProbeReasonCode");
var hybridExecution = ReadDiagnosticBool(result.Diagnostics, "hybridExecution");
var hasFallbackMarker = HasHybridFallbackMarker(result, backendRoute, routeReasonCode, result.Diagnostics);

matrixEntries.Add(new ActionStatusEntry(
ProfileId: profileId,
ActionId: actionSpec.ActionId,
Outcome: result.Succeeded ? "Passed" : "Failed",
Message: result.Message,
BackendRoute: backendRoute,
RouteReasonCode: routeReasonCode,
CapabilityProbeReasonCode: capabilityProbeReasonCode,
HybridExecution: hybridExecution,
HasFallbackMarker: hasFallbackMarker,
SkipReasonCode: null));

_output.WriteLine(
$"matrix profile={profileId} action={actionSpec.ActionId} success={result.Succeeded} backend={backendRoute} route={routeReasonCode} cap={capabilityProbeReasonCode} hybrid={hybridExecution} fallbackMarker={hasFallbackMarker} msg={result.Message}");

AssertPromotedActionExecution(
result,
profileId,
actionSpec.ActionId,
backendRoute,
routeReasonCode,
capabilityProbeReasonCode,
hybridExecution,
hasFallbackMarker);
var payloads = actionSpec.BuildPayloads();
payloads.Should().NotBeEmpty(
because: $"promoted action '{actionSpec.ActionId}' should include at least one payload step for profile '{profileId}'.");

for (var index = 0; index < payloads.Count; index++)
{
var payload = payloads[index];
var actionIdWithStep = payloads.Count > 1
? $"{actionSpec.ActionId}[{index + 1}/{payloads.Count}]"
: actionSpec.ActionId;
var request = new ActionExecutionRequest(action, payload, profileId, mode);
var result = await runtime.ExecuteAsync(request);
var backendRoute = ReadDiagnosticString(result.Diagnostics, "backendRoute");
var routeReasonCode = ReadDiagnosticString(result.Diagnostics, "routeReasonCode");
var capabilityProbeReasonCode = ReadDiagnosticString(result.Diagnostics, "capabilityProbeReasonCode");
var hybridExecution = ReadDiagnosticBool(result.Diagnostics, "hybridExecution");
var hasFallbackMarker = HasHybridFallbackMarker(result, backendRoute, routeReasonCode, result.Diagnostics);

matrixEntries.Add(new ActionStatusEntry(
ProfileId: profileId,
ActionId: actionIdWithStep,
Outcome: result.Succeeded ? "Passed" : "Failed",
Message: result.Message,
BackendRoute: backendRoute,
RouteReasonCode: routeReasonCode,
CapabilityProbeReasonCode: capabilityProbeReasonCode,
HybridExecution: hybridExecution,
HasFallbackMarker: hasFallbackMarker,
SkipReasonCode: null));

_output.WriteLine(
$"matrix profile={profileId} action={actionIdWithStep} success={result.Succeeded} backend={backendRoute} route={routeReasonCode} cap={capabilityProbeReasonCode} hybrid={hybridExecution} fallbackMarker={hasFallbackMarker} msg={result.Message}");

AssertPromotedActionExecution(
result,
profileId,
actionIdWithStep,
backendRoute,
routeReasonCode,
capabilityProbeReasonCode,
hybridExecution,
hasFallbackMarker);
}
}

private bool TrySkipUnavailableProfileContext(
Expand Down Expand Up @@ -432,7 +463,7 @@ private static bool TryGetLiveOutputDirectory(out string outputDir)
return true;
}

private sealed record PromotedActionSpec(string ActionId, Func<JsonObject> BuildPayload);
private sealed record PromotedActionSpec(string ActionId, Func<IReadOnlyList<JsonObject>> BuildPayloads);

private sealed record RuntimeDependencies(
FileSystemProfileRepository ProfileRepository,
Expand Down
Loading
Loading