fix(headless): S4 report UnknownCommand/Dropped and never let submit escape DrainDue
HeadlessConsoleController.Handle called _submit(rawLine) bare: an UnknownCommand/Dropped outcome printed nothing (the operator had no way to tell their line did nothing), and any exception from the submit callback would propagate out of DrainDue into the scheduler's per-session quarantine catch, faulting the whole session over one console typo. Wrap the submit in try/catch and report both cases with a visible line, mirroring LoginCommandSequence.DrainDue's own reporting for login-line failures. N1: also corrected this class's own <remarks> doc comment, which described the dispatch order as "plugin-verb registry -> client/server slash commands" -- the real ChatCommandRouter.Submit order is retail's client-command catalog, then local /help, then plugin verbs, then the unregistered-channel-tag fallback, then an explicit server command, then chat. UnknownOrDroppedOutcomePrintsAVisibleLine and SubmitFailurePrintsALineAndNeverEscapesDrainDue were shown to fail against the prior bare `_submit(rawLine);` call: the outcome tests found nothing printed, and the failure test caught the InvalidOperationException escaping DrainDue itself rather than being reported as a line. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
cb076c6558
commit
f0b7a136b2
2 changed files with 78 additions and 4 deletions
|
|
@ -15,9 +15,12 @@ namespace AcDream.Headless.Hosting;
|
|||
/// "Control" section) — they never reach <see cref="ChatCommandRouter"/>,
|
||||
/// matching retail's own client-local commands. Every other line goes
|
||||
/// through <paramref name="submit"/>, which a production caller binds to
|
||||
/// <c>HeadlessSessionHost.SubmitConsoleLine</c> — the exact plugin-verb
|
||||
/// registry → client/server slash-command pipeline
|
||||
/// <c>LoginCommandSequence</c> and the graphical chat box both already use.
|
||||
/// <c>HeadlessSessionHost.SubmitConsoleLine</c> — the exact
|
||||
/// <see cref="ChatCommandRouter.Submit"/> pipeline (retail's client-command
|
||||
/// catalog first, then local <c>/help</c>, then the plugin-verb registry,
|
||||
/// then the retail unregistered-channel-tag fallback, then an explicit
|
||||
/// server command, then plain chat) <c>LoginCommandSequence</c> and the
|
||||
/// graphical chat box both already use.
|
||||
/// </remarks>
|
||||
internal sealed class HeadlessConsoleController : IDisposable
|
||||
{
|
||||
|
|
@ -83,7 +86,24 @@ internal sealed class HeadlessConsoleController : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
_submit(rawLine);
|
||||
// S4 (2026-09-07 review round): mirrors
|
||||
// LoginCommandSequence.DrainDue's own try/catch and
|
||||
// UnknownCommand/Dropped reporting — a console typo (a bad line, a
|
||||
// downstream bug in a plugin verb handler) must never escape to the
|
||||
// scheduler's per-session quarantine catch and fault the whole
|
||||
// session, and the operator deserves the same "this line did
|
||||
// nothing" signal LoginCommandSequence already gives a login-line
|
||||
// failure.
|
||||
try
|
||||
{
|
||||
SubmitOutcome outcome = _submit(rawLine);
|
||||
if (outcome is SubmitOutcome.UnknownCommand or SubmitOutcome.Dropped)
|
||||
WriteLine($"not handled ({outcome}): {rawLine}");
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
WriteLine($"command failed: {error.GetBaseException().Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteLine(string text)
|
||||
|
|
|
|||
|
|
@ -257,6 +257,60 @@ public sealed class HeadlessConsoleTests
|
|||
output.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S4: <see cref="SubmitOutcome.UnknownCommand"/> and
|
||||
/// <see cref="SubmitOutcome.Dropped"/> get a visible console line —
|
||||
/// matching <c>LoginCommandSequence.DrainDue</c>'s own reporting for the
|
||||
/// same two outcomes — instead of silently doing nothing.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(SubmitOutcome.UnknownCommand)]
|
||||
[InlineData(SubmitOutcome.Dropped)]
|
||||
public void UnknownOrDroppedOutcomePrintsAVisibleLine(SubmitOutcome outcome)
|
||||
{
|
||||
using var input = new System.IO.StringReader("garbage" + Environment.NewLine);
|
||||
var output = new StringWriter();
|
||||
using var quit = new CancellationTokenSource();
|
||||
using var controller = new HeadlessConsoleController(
|
||||
input,
|
||||
output,
|
||||
_ => outcome,
|
||||
() => string.Empty,
|
||||
quit);
|
||||
|
||||
Assert.True(WaitForEndOfInput(controller));
|
||||
controller.DrainDue();
|
||||
|
||||
Assert.Contains("garbage", output.ToString());
|
||||
Assert.Contains(outcome.ToString(), output.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S4: a throwing submit callback (a console typo hitting a downstream
|
||||
/// bug in a plugin verb handler, say) never escapes <c>DrainDue</c> —
|
||||
/// it must never reach the scheduler's per-session quarantine catch and
|
||||
/// fault the whole session over one bad console line. Mirrors
|
||||
/// <c>LoginCommandSequence.DrainDue</c>'s own try/catch.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SubmitFailurePrintsALineAndNeverEscapesDrainDue()
|
||||
{
|
||||
using var input = new System.IO.StringReader("boom" + Environment.NewLine);
|
||||
var output = new StringWriter();
|
||||
using var quit = new CancellationTokenSource();
|
||||
using var controller = new HeadlessConsoleController(
|
||||
input,
|
||||
output,
|
||||
_ => throw new InvalidOperationException("fixture failure"),
|
||||
() => string.Empty,
|
||||
quit);
|
||||
|
||||
Assert.True(WaitForEndOfInput(controller));
|
||||
controller.DrainDue();
|
||||
|
||||
Assert.Contains("fixture failure", output.ToString());
|
||||
}
|
||||
|
||||
// ── HeadlessConsoleChatFormatter: channel-prefixed rendering ─────────
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue