wip: Campaign LA LA1 fix round — INCOMPLETE, stopped mid-task
Agent was stopped for token budget partway through the LA1 review fix round. Landed here: F1 best-effort SessionStatusWriter, F2 App reader tolerance (paths/mode), F5 argument-parsing hardening, plus new tests. NOT DONE: F4 shared-fixture production shape (was the next step), F3 reconnect disconnected edge + recorded limitation, F6 exited idempotency/reasons, F7 structural redaction test, F8 platform-guard test + comment fix, optional RuntimeOptions PrintMembers redaction. Build/test state UNVERIFIED at this commit. Next session: finish the remaining findings, run the suites, then narrow re-review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c9fc7f4a66
commit
75a6724d5b
9 changed files with 628 additions and 52 deletions
|
|
@ -0,0 +1,73 @@
|
|||
namespace AcDream.App.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA slice LA1 review fix (F5): extracted from <c>Program.cs</c>'s
|
||||
/// top-level-statement local functions so the trailing-flag edge case is
|
||||
/// unit testable — a top-level program's local functions are compiler-
|
||||
/// synthesized private members of the generated <c>Program</c> class with
|
||||
/// no stable surface a test assembly can reach.
|
||||
/// </summary>
|
||||
internal static class SessionConfigArgumentParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Finds <paramref name="flag"/> in <paramref name="arguments"/> and
|
||||
/// returns its value. Three distinct outcomes, distinguished by
|
||||
/// <paramref name="present"/> and the return value together:
|
||||
/// <list type="bullet">
|
||||
/// <item>flag absent: <paramref name="present"/> = <see langword="false"/>,
|
||||
/// returns <see langword="null"/> — the caller's env-var/positional
|
||||
/// fallback stays in effect, unchanged from before this flag
|
||||
/// existed.</item>
|
||||
/// <item>flag present with a following value: <paramref name="present"/>
|
||||
/// = <see langword="true"/>, returns that value.</item>
|
||||
/// <item>flag present but is the LAST argument, with nothing after it:
|
||||
/// <paramref name="present"/> = <see langword="true"/>, returns
|
||||
/// <see langword="null"/> — the caller MUST treat this as a hard error
|
||||
/// (the flag was typed but its value was not), never silently fall
|
||||
/// through to the flag-absent path.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
internal static string? ExtractFlagValue(
|
||||
string[] arguments,
|
||||
string flag,
|
||||
out bool present)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(arguments);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(flag);
|
||||
|
||||
for (int i = 0; i < arguments.Length; i++)
|
||||
{
|
||||
if (!string.Equals(arguments[i], flag, StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
present = true;
|
||||
return i == arguments.Length - 1 ? null : arguments[i + 1];
|
||||
}
|
||||
|
||||
present = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Returns <paramref name="arguments"/> with <paramref name="flag"/>
|
||||
/// and its following value (if any) removed. A trailing, valueless flag
|
||||
/// is dropped on its own — this helper only strips arguments, it does
|
||||
/// not decide whether a trailing flag is an error (see
|
||||
/// <see cref="ExtractFlagValue"/>'s <c>present</c> output for that).</summary>
|
||||
internal static string[] WithoutFlagAndValue(string[] arguments, string flag)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(arguments);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(flag);
|
||||
|
||||
var result = new List<string>(arguments.Length);
|
||||
for (int i = 0; i < arguments.Length; i++)
|
||||
{
|
||||
if (string.Equals(arguments[i], flag, StringComparison.Ordinal))
|
||||
{
|
||||
i++; // also skip the flag's value, if any
|
||||
continue;
|
||||
}
|
||||
result.Add(arguments[i]);
|
||||
}
|
||||
return [.. result];
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,26 @@ internal sealed class SessionConfiguration
|
|||
internal sealed class SessionProcessSettings
|
||||
{
|
||||
public SessionContentDescriptor? Content { get; init; }
|
||||
|
||||
/// <summary>Campaign LA slice LA1 review fix (F2): accepted so the SAME
|
||||
/// document also satisfies the Headless loader's own
|
||||
/// <c>process.paths</c> member (<c>HeadlessPathOverrides</c>) — parsed
|
||||
/// and ignored here, exactly like <see cref="SessionDescriptor.Policy"/>
|
||||
/// and <see cref="SessionDescriptor.CharacterOptions"/> below. App has
|
||||
/// no config/data/cache directory override concept of its own (those
|
||||
/// come from <c>ApplicationPathSet</c>/env vars on this host); only the
|
||||
/// Headless host consumes overrides composed under this key.</summary>
|
||||
public SessionProcessPathOverrides? Paths { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Accepted-but-ignored mirror of Headless's
|
||||
/// <c>HeadlessPathOverrides</c> shape — see
|
||||
/// <see cref="SessionProcessSettings.Paths"/>.</summary>
|
||||
internal sealed class SessionProcessPathOverrides
|
||||
{
|
||||
public string? ConfigDirectory { get; init; }
|
||||
public string? DataDirectory { get; init; }
|
||||
public string? CacheDirectory { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class SessionContentDescriptor
|
||||
|
|
@ -75,6 +95,19 @@ internal sealed record SessionDescriptor
|
|||
/// App has no bot-policy concept.</summary>
|
||||
public SessionPolicyDescriptor? Policy { get; init; }
|
||||
|
||||
/// <summary>Campaign LA slice LA1 review fix (F2): pinned-contract
|
||||
/// mode discriminator. ABSENT means today's ONLY App behavior — an
|
||||
/// ordinary play session — so every document written before this field
|
||||
/// existed keeps parsing unchanged. <c>"probe"</c> (LA2's connect
|
||||
/// ▸ characterList ▸ graceful-disconnect flow, no EnterWorld) is
|
||||
/// HEADLESS-ONLY; the App loader rejects it with an explicit message
|
||||
/// naming the field rather than the caller ever seeing a raw unmapped-
|
||||
/// member <see cref="System.Text.Json.JsonException"/>. Any other value
|
||||
/// is a configuration error — the pinned contract defines no other
|
||||
/// mode literal, so a document is either silent about mode (play) or
|
||||
/// says "probe" exactly.</summary>
|
||||
public string? Mode { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public SessionCredentialDescriptor Credential { get; init; } = new();
|
||||
|
||||
|
|
|
|||
|
|
@ -154,5 +154,31 @@ internal static class SessionConfigurationLoader
|
|||
throw new SessionConfigurationException(
|
||||
$"Session '{session.Id}' statusFile must be a non-empty path when present.");
|
||||
}
|
||||
|
||||
ValidateMode(session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA slice LA1 review fix (F2): <c>mode</c> is Headless-only
|
||||
/// on the App host — the graphical host has no probe concept (LA2
|
||||
/// builds the probe in Headless only). An absent field is today's ONLY
|
||||
/// App behavior (play); <c>"probe"</c> gets a specific, actionable
|
||||
/// message instead of a cryptic unmapped-member JSON error; anything
|
||||
/// else is a plain configuration error.
|
||||
/// </summary>
|
||||
private static void ValidateMode(SessionDescriptor session)
|
||||
{
|
||||
if (session.Mode is null)
|
||||
return;
|
||||
|
||||
if (string.Equals(session.Mode, "probe", StringComparison.Ordinal))
|
||||
{
|
||||
throw new SessionConfigurationException(
|
||||
$"Session '{session.Id}' has mode 'probe'; probe sessions "
|
||||
+ "are headless-only and cannot run on the graphical host.");
|
||||
}
|
||||
|
||||
throw new SessionConfigurationException(
|
||||
$"Session '{session.Id}' has unsupported mode '{session.Mode}'.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue