fix(launcher): Campaign LA gate-round-1 review findings F1-F6 + hardening
F1: the crash reporter comment claimed the launcher never holds a password in any field - false (ProfileEditorDialogViewModel, AccountProfile.Password, StartRequest.Password). Reworded to the true, narrower invariant (no throw site interpolates a credential VALUE into an exception message) and pinned it with CrashReportNeverContainsAStoredPassword: a real STJ failure over a profiles document containing a known password, corrupted after the credential, must yield a crash file with the stack and without the value. F2: the co-deploy Inputs covered only Bake own sources; a Content edit never refreshed the 83 MB exe. Now the full reference closure. Fixing it surfaced two more incrementality traps, both fixed and comment-documented: SkipUnchangedFiles left the output older than the triggering input (target re-ran forever - added an explicit Touch), and %(Item.Metadata) in a plain Include does not batch (the literal percent-text became a permanently out-of-date phantom input - globs are now spelled per project). Verified: Core edit retriggers, then two consecutive clean incremental builds. F3: RID publishes ran BOTH co-deploy paths (two self-contained bake publishes). Build-time target now guarded on _IsPublishing; verified a real win-x64 publish runs zero build-target co-deploys and still ships both exes. F4: comment misattributed PublishBakeTool=false to CI lanes; it is target-local recursion guarding. F5: the x:Name reflection sweep now walks the markup as XML and tolerates template-scoped names (no generated field exists for those). F6: dead using removed. Hardening: the crash reporter positional --data-dir fallback requires a fully-qualified path so a relative or flag-shaped value cannot create ./crash-reports at an arbitrary CWD. Launcher 67/67, Launcher.Core 317/317. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1f87acf1af
commit
981e168fb9
6 changed files with 178 additions and 32 deletions
|
|
@ -1,5 +1,5 @@
|
|||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using AcDream.Launcher.Core.Installation;
|
||||
using AcDream.Launcher.Core.Launching;
|
||||
using AcDream.Launcher.Core.Orchestration;
|
||||
|
|
@ -74,10 +74,23 @@ public sealed class MainWindowViewTests
|
|||
"src",
|
||||
"AcDream.Launcher",
|
||||
"MainWindow.axaml");
|
||||
string markup = File.ReadAllText(markupPath);
|
||||
List<string> names = Regex
|
||||
.Matches(markup, "x:Name=\"([^\"]+)\"")
|
||||
.Select(match => match.Groups[1].Value)
|
||||
// Walk the markup as XML rather than regexing the raw text:
|
||||
// template-scoped names (inside a DataTemplate/ControlTemplate/
|
||||
// ItemTemplate) get NO generated backing field, so demanding one
|
||||
// would false-fail the first time a template gains an x:Name
|
||||
// (gate-round-1 review F5 — latent today, MainWindow has two
|
||||
// templates with none inside).
|
||||
XDocument document = XDocument.Load(markupPath);
|
||||
XNamespace x = "http://schemas.microsoft.com/winfx/2006/xaml";
|
||||
List<string> names = document
|
||||
.Descendants()
|
||||
.Where(element => element.Attribute(x + "Name") is not null)
|
||||
.Where(element => !element
|
||||
.Ancestors()
|
||||
.Any(ancestor => ancestor.Name.LocalName.EndsWith(
|
||||
"Template",
|
||||
StringComparison.Ordinal)))
|
||||
.Select(element => element.Attribute(x + "Name")!.Value)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
|
|
@ -260,6 +273,74 @@ public sealed class MainWindowViewTests
|
|||
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gate-round-1 review F1: the crash reporter's safety rests on the
|
||||
/// invariant that no code path interpolates a credential VALUE into an
|
||||
/// exception message — the launcher genuinely holds passwords
|
||||
/// (ProfileEditorDialogViewModel, AccountProfile.Password,
|
||||
/// StartRequest.Password), so "no password in any field" was never the
|
||||
/// guarantee. This test pins the real one against the most
|
||||
/// credential-adjacent realistic failure: a profiles-shaped document
|
||||
/// that CONTAINS the password and is corrupted AFTER it, so the JSON
|
||||
/// parser has consumed the credential value before throwing.
|
||||
/// System.Text.Json quotes paths and positions, never values — if that
|
||||
/// (or any future throw site) ever changes, this fails and the sink
|
||||
/// needs the status-stream's credential scanning.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CrashReportNeverContainsAStoredPassword()
|
||||
{
|
||||
string root = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"acdream-tests",
|
||||
Path.GetRandomFileName());
|
||||
string dataDirectory = Path.Combine(root, "data");
|
||||
const string password = "hunter2-gate-round-1-secret";
|
||||
string corruptProfiles =
|
||||
"{ \"version\": 1, \"servers\": [ { \"name\": \"s\", \"host\": \"h\", "
|
||||
+ "\"port\": 9000, \"accounts\": [ { \"account\": \"a\", \"password\": \""
|
||||
+ password
|
||||
+ "\", \"characters\": [ } ] } ] }";
|
||||
|
||||
Exception failure;
|
||||
try
|
||||
{
|
||||
_ = System.Text.Json.JsonSerializer.Deserialize<System.Text.Json.JsonElement>(
|
||||
corruptProfiles);
|
||||
throw new InvalidOperationException(
|
||||
"The corrupt fixture unexpectedly parsed; the test premise is broken.");
|
||||
}
|
||||
catch (System.Text.Json.JsonException jsonFailure)
|
||||
{
|
||||
failure = new InvalidOperationException(
|
||||
"Profile load failed during startup.",
|
||||
jsonFailure);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string? report = Program.TryWriteCrashReport(
|
||||
["--data-dir", dataDirectory],
|
||||
failure);
|
||||
|
||||
Assert.NotNull(report);
|
||||
// Isolation re-pinned: the report must land under the caller's
|
||||
// --data-dir, never the machine's real data root.
|
||||
Assert.StartsWith(dataDirectory, report, StringComparison.OrdinalIgnoreCase);
|
||||
string content = File.ReadAllText(report);
|
||||
Assert.Contains("JsonException", content);
|
||||
Assert.Contains(" at ", content);
|
||||
Assert.DoesNotContain(password, content, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(root))
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal no-op orchestrator. These tests exercise MainWindow's own
|
||||
/// dispatcher/focus wiring, not orchestrator behavior (already covered
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue