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:
Erik 2026-08-14 16:32:52 +02:00
parent c9fc7f4a66
commit 75a6724d5b
9 changed files with 628 additions and 52 deletions

View file

@ -97,8 +97,22 @@ public sealed class SessionStatusWriterTests
writer.Started("s1");
}
/// <summary>
/// F7 (Campaign LA LA1 review fix round): replaces the earlier
/// "DoesNotContain 'hunter2'/'password'" assertion, which could never
/// actually fail — no writer method below accepts a credential-shaped
/// parameter in the first place, so the absence of those literal strings
/// proved nothing about the SHAPE of what gets serialized. This test
/// asserts the structural claim that actually backs the "never write
/// credential material into this stream" contract: each event kind
/// serializes EXACTLY its pinned property set — the shared envelope
/// (<c>v</c>/<c>e</c>/<c>t</c>/<c>sessionId</c>) plus that event's own
/// named fields, nothing else. An extra property (a smuggled password,
/// or any other accidental field) fails this test by construction,
/// regardless of what value it carries.
/// </summary>
[Fact]
public void PasswordNeverAppearsInTheStatusStream()
public void EachEventSerializesExactlyItsPinnedPropertySetAndNothingElse()
{
using TemporaryFile file = TemporaryFile.Create();
var writer = new SessionStatusWriter(file.Path);
@ -115,9 +129,110 @@ public sealed class SessionStatusWriterTests
writer.Disconnected("bot", "stopped");
writer.Exited("bot", 0, "disposed");
string contents = File.ReadAllText(file.Path);
Assert.DoesNotContain("hunter2", contents, StringComparison.Ordinal);
Assert.DoesNotContain("password", contents, StringComparison.OrdinalIgnoreCase);
string[] lines = File.ReadAllLines(file.Path);
Assert.Equal(6, lines.Length);
AssertExactProperties(lines[0], "v", "e", "t", "sessionId");
AssertExactProperties(lines[1], "v", "e", "t", "sessionId");
AssertExactProperties(
lines[2],
"v", "e", "t", "sessionId", "accountName", "slotCount", "characters");
AssertExactProperties(
lines[3], "v", "e", "t", "sessionId", "characterId", "characterName");
AssertExactProperties(lines[4], "v", "e", "t", "sessionId", "reason");
AssertExactProperties(lines[5], "v", "e", "t", "sessionId", "code", "reason");
// The nested characters[] entries are exact too — the exact shape a
// password could otherwise be smuggled through.
JsonElement character = Parse(lines[2]).GetProperty("characters")[0];
AssertExactProperties(character, "id", "name", "secondsGreyedOut");
}
private static void AssertExactProperties(string line, params string[] expected) =>
AssertExactProperties(Parse(line), expected);
private static void AssertExactProperties(JsonElement element, params string[] expected)
{
string[] actual = element.EnumerateObject()
.Select(static property => property.Name)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
string[] sortedExpected = expected
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
Assert.Equal(sortedExpected, actual);
}
/// <summary>
/// F1 (Campaign LA LA1 review fix round): a status file whose parent
/// directory does not exist yet — the expected first-run shape of
/// <c>.../launcher/sessions/&lt;id&gt;/status.jsonl</c> on a fresh cache
/// dir — must be created lazily rather than throwing
/// <see cref="DirectoryNotFoundException"/> out of the transaction the
/// writer is merely observing.
/// </summary>
[Fact]
public void MissingParentDirectoryIsCreatedAndEventsFlow()
{
string root = Path.Combine(
Path.GetTempPath(),
$"acdream-status-root-{Guid.NewGuid():N}");
string path = Path.Combine(root, "nested", "sessions", "s1", "status.jsonl");
try
{
Assert.False(Directory.Exists(Path.GetDirectoryName(path)));
var writer = new SessionStatusWriter(path);
writer.Started("s1");
writer.Connected("s1");
Assert.True(writer.IsEnabled);
string[] lines = File.ReadAllLines(path);
Assert.Equal(2, lines.Length);
Assert.Contains("\"started\"", lines[0]);
Assert.Contains("\"connected\"", lines[1]);
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
/// <summary>
/// F1: a path whose PARENT SEGMENT already exists as an ordinary file
/// (so <see cref="Directory.CreateDirectory"/> cannot turn it into a
/// directory) is exactly the "unwritable path" case the review asked
/// for — the writer must latch itself off instead of throwing, and every
/// subsequent call must stay a cheap no-op.
/// </summary>
[Fact]
public void ParentSegmentIsAFileLatchesTheWriterInsteadOfThrowing()
{
string blocker = Path.Combine(
Path.GetTempPath(),
$"acdream-status-blocker-{Guid.NewGuid():N}");
File.WriteAllText(blocker, "not a directory");
string path = Path.Combine(blocker, "status.jsonl");
try
{
var writer = new SessionStatusWriter(path);
Assert.True(writer.IsEnabled);
// Must not throw — the writer swallows its own I/O failure and
// latches off instead of failing the caller's transaction.
writer.Started("s1");
Assert.False(writer.IsEnabled);
// Latched-off calls stay cheap no-ops — no exception, no retry.
writer.Connected("s1");
writer.Exited("s1", 0, "disposed");
}
finally
{
if (File.Exists(blocker))
File.Delete(blocker);
}
}
[Fact]