feat(presentation): move the companion's decisions into a tested, UI-free assembly #34

Merged
james.maes merged 25 commits from feat/2.0-presentation-assembly-on-release-20261110 into release/2.0 2026-11-10 23:13:47 +00:00
Owner

Moves the companion's decisions out of its window classes into a new UI-free assembly, Qqq.PrintAgent.Presentation, tested at 100% line coverage (922 of 922 lines, 352 tests) and gated there. The wizard and the server tree will be built on it; neither is in this change.

What is in the assembly

  • Service/ServiceClient: one typed client for the service pipe. Every request returns a ServiceResult that says whether it succeeded, failed, was never sent, or left without a usable answer. The views no longer parse replies three ways or match text by prefix.
  • SignIn/SignInFlow: the connection dialog's logic as a state machine (discovery, password and browser sign-in, enrollment, sign-out, the certificate exception), one operation at a time through one runner, cancellable, quiet after dispose.
  • Shell/, Jobs/, Printers/, Settings/: what the footer, the job list, the printer rows and the settings views show and enable.
  • The views (MainWindow.*.cs) call these and hold no decisions.

Changes in the Windows layer

  • AgentPipeClient reports whether a request left (AgentPipeNotSentException when it did not), so "nothing was sent" is never claimed after the first byte.
  • AgentPipePeer tells a service that stopped or restarted during the identity check from a peer that was checked and is not the installed service. No request is ever sent to an unverified peer.
  • SignInSentences is the one definition of the sentences the service and the client share.
  • AgentControlState.AuthorizationRequired and its last reader are removed (it was always false).
  • MsiUpdateService shows a "not ready" or "interrupted" update preparation in the service's own words.

What an operator can notice (all declared in CHANGELOG.md)

  • A sign-in or sign-out that was handed to the service and got no usable answer says it could not be confirmed and to check the status, instead of "canceled" or "service unavailable". The same for a certificate-exception save, whose checkbox stays undetermined until the setting is read again.
  • A printer or job action that never left says "Request not sent"; one that left and was abandoned still says the result could not be confirmed.
  • Clicking while another operation runs says so and keeps what was typed. Closing the window mid-operation shows and sends nothing more.
  • After a confirmed sign-in the sign-in form is gone until Discover is chosen again; the companion keeps no session once the service has it.
  • Failures the window used to let escape (identity check failures, an oversize message, a malformed browser callback) show the usual message.

Verification, local, on 6c579cc (this head minus a CLAUDE.md-only commit), on 2375195: build/Verify.ps1 passed three times, first attempt each: 793 Core, 629 Windows/command-line and 352 presentation tests, zero warnings; Core 100%, command line 100%, presentation 100%, Windows 70.7%. The native layout host passed.

Not verified: the interactive UI test suite was not run. Needing the real desktop: the undetermined checkbox and what its first click sends, "Another operation is still running" in the dialog, "Request not sent" against a stopped service, the update dialog's new sentences, a real browser sign-in and provider sign-out.

Review: independent, five passes in all (two on the first version, three on the fixes). The third found one blocker (a sent-but-unanswered browser sign-in shown as canceled, carried over from 1.2.1) and six should-fix items; the last two passes found nothing at should-fix or above. Three nits remain, recorded for the wizard work: announcement order in the confirmed hand-over, a log line every five seconds while an unconfirmed certificate setting cannot be re-read, and "try again when the service can be reached" shown for an oversize request.

🤖 Generated with Claude Code

Moves the companion's decisions out of its window classes into a new UI-free assembly, `Qqq.PrintAgent.Presentation`, tested at 100% line coverage (922 of 922 lines, 352 tests) and gated there. The wizard and the server tree will be built on it; neither is in this change. **What is in the assembly** - `Service/ServiceClient`: one typed client for the service pipe. Every request returns a `ServiceResult` that says whether it succeeded, failed, was never sent, or left without a usable answer. The views no longer parse replies three ways or match text by prefix. - `SignIn/SignInFlow`: the connection dialog's logic as a state machine (discovery, password and browser sign-in, enrollment, sign-out, the certificate exception), one operation at a time through one runner, cancellable, quiet after dispose. - `Shell/`, `Jobs/`, `Printers/`, `Settings/`: what the footer, the job list, the printer rows and the settings views show and enable. - The views (`MainWindow.*.cs`) call these and hold no decisions. **Changes in the Windows layer** - `AgentPipeClient` reports whether a request left (`AgentPipeNotSentException` when it did not), so "nothing was sent" is never claimed after the first byte. - `AgentPipePeer` tells a service that stopped or restarted during the identity check from a peer that was checked and is not the installed service. No request is ever sent to an unverified peer. - `SignInSentences` is the one definition of the sentences the service and the client share. - `AgentControlState.AuthorizationRequired` and its last reader are removed (it was always false). - `MsiUpdateService` shows a "not ready" or "interrupted" update preparation in the service's own words. **What an operator can notice** (all declared in `CHANGELOG.md`) - A sign-in or sign-out that was handed to the service and got no usable answer says it could not be confirmed and to check the status, instead of "canceled" or "service unavailable". The same for a certificate-exception save, whose checkbox stays undetermined until the setting is read again. - A printer or job action that never left says "Request not sent"; one that left and was abandoned still says the result could not be confirmed. - Clicking while another operation runs says so and keeps what was typed. Closing the window mid-operation shows and sends nothing more. - After a confirmed sign-in the sign-in form is gone until Discover is chosen again; the companion keeps no session once the service has it. - Failures the window used to let escape (identity check failures, an oversize message, a malformed browser callback) show the usual message. **Verification**, local, on `6c579cc` (this head minus a `CLAUDE.md`-only commit), on `2375195`: `build/Verify.ps1` passed three times, first attempt each: 793 Core, 629 Windows/command-line and 352 presentation tests, zero warnings; Core 100%, command line 100%, presentation 100%, Windows 70.7%. The native layout host passed. **Not verified**: the interactive UI test suite was not run. Needing the real desktop: the undetermined checkbox and what its first click sends, "Another operation is still running" in the dialog, "Request not sent" against a stopped service, the update dialog's new sentences, a real browser sign-in and provider sign-out. **Review**: independent, five passes in all (two on the first version, three on the fixes). The third found one blocker (a sent-but-unanswered browser sign-in shown as canceled, carried over from 1.2.1) and six should-fix items; the last two passes found nothing at should-fix or above. Three nits remain, recorded for the wizard work: announcement order in the confirmed hand-over, a log line every five seconds while an unconfirmed certificate setting cannot be re-read, and "try again when the service can be reached" shown for an oversize request. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
james.maes added 25 commits 2026-11-10 23:13:45 +00:00
Adds src/Qqq.PrintAgent.Presentation and tests/Qqq.PrintAgent.Presentation.Tests to both
solutions, to the suites build/Verify.ps1 runs and to build/minimum-coverage.json at 100.
TransportFormatting and JobDetailsRequestTracker move into it with their tests, which the
gate never ran while they lived in the interactive UI test project; three WebSocket state
wordings the old tests did not reach are now asserted.

The companion and the layout host reference the new assembly; no visible text changes.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
ServiceClient wraps the raw send delegate and returns a ServiceResult: success with a
payload, a service-reported error with its message, or unavailable with the failure kind.
The 'Error:' marker is recognised one way (with or without the space, any case), the
'Enrolled device' and 'Signed in to <server>.' openings decide success in one place, and
IOException, UnauthorizedAccessException, OperationCanceledException, Win32Exception and
InvalidDataException become results instead of exceptions. Swallowed failures go to an
injected log: a warning the first time, debug detail while the same failure repeats, and
one line when the service answers again. A CancellationToken ends the wait. The wire
format and the service are unchanged, and nothing calls the client yet.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Pure types for the decisions and wording that live in the views today, each asserted by
unit tests: PrinterRow, PrinterDetails, PrinterActions, PrinterSort, PrintersSummary and
TestPrintOptions; JobListState (paging, sorting, query versions), JobRow, JobActions,
JobPageSummary and JobDetailsContent; ShellStatus, HealthStatus, TransportStatus,
ShellFeedback and ShellPages; UpdateStatus, LogSettingsForm and MachineStatus; and the
shared SortHeader, RowDisclosure, TimestampFormat, FeedbackMessage and ConfirmationPrompt.
Times are formatted through an injected TimeProvider and culture so the tests do not
depend on the machine. The views still carry their own copies; the next commits make them
call these types.

Replayed onto release/2.0: the control state there no longer carries a
demand for authorization (the property is get-only and always false), so
ShellStatus does not read it and the footer has no sentence for it; missing
authorization is shown from the authentication status.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
The printer and job rows, the details views, the test-print editor and the window's
printers, jobs, sorting, footer, transport, actions, updates, log settings and metadata
parts now copy prepared texts and enablement from the presentation types instead of
deciding them. Requests go through ServiceClient, so the 'Error:' checks, the JSON parsing
and the pipe exception filters leave the code-behind; ServiceRequests builds the
state-changing requests.

Visible differences, all at the edges: a service error shown as 'Action could not be
completed' no longer starts with a space; a status, transport, job or log-settings request
that fails with Win32Exception or InvalidDataException now shows the same feedback as the
other pipe failures instead of escaping the handler; and pipe failures are written to the
activity log. Sign-in, sign-out, enrollment and server trust still use the raw delegate
and move in the next commits.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
SignInFlow owns the sign-in session and decides every transition and message of the
connection dialog: discovery, password sign-in, browser sign-in with its three minute
limit and cancellation, the hand-over to the service with the pairing choice, sign-out
including the identity provider's own sign-out, enrollment by code, and the certificate
exception for one server. The session client, the browser launch and the callback inbox
are behind ISignInSession(Factory), IBrowserLauncher and IBrowserCallbackSource/Inbox, the
time limits run on an injected TimeProvider, and state is observable so a later wizard can
bind to the same logic. Tests drive every path with hand-written fakes and a manual clock
and assert that no password, code, state or token hint reaches the log. The dialog still
runs its own handlers; the next commit puts it on top of the flow.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
The dialog's handlers now pass their fields to SignInFlow, mark the window busy around each
operation and show what the flow announces: which sign-in methods are offered, the Cancel
button of a waiting browser sign-in, the certificate exception and every message. The real
session client, the default browser and the callback pipe are supplied through thin
adapters (QqqSignInSession and its factory, DefaultBrowserLauncher, BrowserCallbackSource
and BrowserCallbackInbox). The raw send delegate is no longer used by the window.

Texts, enablement and AutomationIds are unchanged. Sign-in, sign-out, enrollment and
trust requests that fail with Win32Exception or InvalidDataException are now reported like
the other pipe failures instead of escaping the handler; a failed discovery now disposes
its half-made session at once instead of at the next discovery.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Also makes the log usage test state its culture, since the size is written with the user's
decimal separator and the test must not depend on the machine's language.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
AgentPipeClient gains SendReportingPhaseAsync: a failure before any request byte is written
(the connect limit, a pipe that cannot be opened, an unverified peer, an oversize request)
is thrown as AgentPipeNotSentException with the original failure inside, so a caller can
tell 'nothing was sent' from 'sent, result unknown'. SendAsync keeps its exception types
for the CLI and the updater; the request size is now checked before connecting.

AgentPipePeer.Authenticate turns every failure to carry out the identity check (service
not installed, process ended between connect and check) into its existing untrusted-peer
UnauthorizedAccessException with the cause inside, instead of letting
InvalidOperationException, ArgumentException or Win32Exception escape.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Review items that stand on their own:
- TimestampFormat reads a timestamp without an offset as UTC (AssumeUniversal) instead of
  in the machine's time zone, with a test.
- The job count line for a failed query and the update-check failure message are now
  decisions in the presentation assembly (JobPageSummary.Failure, UpdateStatus.CheckFailed)
  with tests of both branches; the tests that compared a constant with the same literal
  are gone.
- PrinterSort's default-comparer test pins its culture and proves the user's language
  rules are used.
- An architecture test walks the references of the presentation assembly and the product
  assemblies under it and finds no WinUI, Windows App SDK, H.NotifyIcon or AppKit.WinUI.
- JobDetailsRequestTracker says why it exists and documents IsLoading.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Service client:
- A failure is a cause plus whether the request had left. ServiceResult.Unconfirmed is true
  only when it left without a usable answer (reply time limit, pipe closed mid-reply,
  oversize or unreadable reply, a wait the caller ended); a connect time limit, a refused
  pipe, an unverified service and an oversize request are 'nothing sent'. The companion now
  sends through AgentPipeClient.SendReportingPhaseAsync.
- A cancelled caller token is its own outcome (Canceled), logged at debug and not counted
  as the service failing; an abandoned request that fails later is observed and noted.
- SignInAsync returns a SignInConfirmation whose SignInReadiness says whether printing is
  enabled or enrollment is still needed, read from the sentences the service really sends.
- A rejection's Message never carries the 'Error:' marker; Reply keeps the whole text.
- A sign-out reply that opens like a JSON object is never used as text: without a readable
  message it is an unreadable reply, so the provider's token hint cannot be shown or logged.
- Every failure keeps its full exception as debug detail.

Sign-in flow:
- One gate for discovery, password, browser, sign-out, enrollment and both certificate
  operations; a second operation while one runs is refused. New stages Enrolling and
  SigningOut, and IsBusy.
- One lifetime CancellationTokenSource linked into every wait, including the provider
  sign-out wait; Dispose ends them all. Discovery, password, browser, enrollment and
  sign-out take a token.
- The session is discarded as soon as the service confirms the hand-over, and dropping a
  session withdraws the methods it offered, so the flow is never idle with a form offered.
- The browser exchange also catches InvalidDataException from the callback inbox.
- Enrollment uses the phase: never sent means service unavailable, sent without an answer
  means 'could not be confirmed'.
- The entered address is logged without user information, query or fragment; the log lines
  for a hand-over time limit and an unanswered sign-out say what actually happened.

Window: the status refresh again turns any InvalidOperationException or KeyNotFoundException
in its whole body into the unreadable-status message, as it did before the refactor.

Tests: fakes that keep the real session's rules, the missing negative cases, and a test
that the flow raises its events on the caller's synchronization context.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
CHANGELOG lists every visible and logged difference, including the two deliberate ones the
review asked for (no session kept after sign-in; 'not sent' told apart from 'sent, result
unknown'). CONTRIBUTING and DEVELOPMENT name tests/Qqq.PrintAgent.Presentation.Tests, its
quick loop and its 100% bar.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
The peer check answered every failure with "not the installed Windows print
service ... repair the machine installation", including a service that
stopped or restarted between the connection and the check. The check now
asks whether the process that served the pipe is still running: if it has
ended, the operator is told the service stopped and to try again; if it is
still running and did not pass, the one refusal is kept. Both remain
UnauthorizedAccessException, so the CLI and the updater behave as before.

The phase-reporting send takes the check of the other end and the request
limit as internal parameters, so a test pipe server can accept the
connection, read the request and then close, stall past the limit or answer
too much. None of those may be reported as "not sent"; a failure before the
first byte is. Moving the "writing" mark after the write fails three of the
new tests.

Also removes the blank lines at the end of AgentPipeClient.cs and squares
the comment box in AgentPipePeer.cs.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
ServiceClient ran the send before looking at the caller's token, so a
request could leave for a caller that was already gone. A token that is
already cancelled now returns a "not started" result (canceled, not
unconfirmed) without calling the send.

Only AgentPipeNotSentException may say nothing was sent. An unwrapped
UnauthorizedAccessException or Win32Exception used to be classified as
"nothing sent" by its type alone; it is now treated as possibly sent.

A sign-out document whose provider part is incomplete made the document's
constructor throw ArgumentNullException out of the client. Every typed
reply is now read through one helper that turns that into an unreadable
reply, and an unreadable reply is logged as "was answered, but the answer
could not be read" instead of "did not complete".

The two sentences that say what a sign-in achieved were spelled in the
service and again in the client. They are now public constants in
SignInSentences (Windows layer); the service's two literals are replaced by
them and the client reads readiness from them, with a test that pins the
wire text and the mapping.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Blocker: when the browser callback had arrived, the sign-in request had
been written and the reply limit ran out, the operator was told "Browser
sign-in canceled. No new sign-in was accepted." although the service may
complete the sign-in. A hand-over that left and brought no usable answer
back is now reported, for password and browser alike, as "Sign-in could not
be confirmed" with what really happened and the instruction to check the
status first. "Canceled" is kept only for a hand-over that never left. The
flow discards its session in that case too, because the service may hold it.

Lifecycle, in one runner all seven operations use:
- Dispose only cancels. The running operation ends itself and discards the
  session on its way out; discarding it under a running call made the real
  client throw ObjectDisposedException, which faulted the password task and
  published "Discovery failed ... disposed". Any failure seen while disposed
  is a quiet end with one debug line.
- The wait is looked at again after every await that precedes a side effect
  (hand-over, opening the browser, completing the login), and nothing raises
  PropertyChanged or reports once the flow is disposed.
- Everything announced at the start is inside the protected part, so a
  subscriber that throws no longer leaves the flow claimed, and the final
  stage is set before the flow is released, so an operation started from the
  IsBusy change keeps its stage.
- A refused operation is no longer silent: it is logged by name, the
  operator is told another operation is still running, and the result says
  "not started", so the window keeps the password or code that was typed.
- The Cancel field is cleared before the browser wait's sources are
  released, so Cancel during the report of a failed exchange does nothing.

Messages say what is true for each cause: enrollment and sign-out no longer
claim "the request timed out" for a closed pipe, an oversize or unreadable
answer, or a wait that was ended. A sign-out that left without a usable
answer is "Sign-out could not be confirmed ... the session may have ended"
instead of "Service unavailable".

Fakes: the scripted session now fails a call it is disposed under and
refuses later calls, as the real client does; the scripted service answers
each operation with the real service's reply; the refused-address failure
is taken from the real client.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
The threading test answered the sign-out with a plain sentence, so the wait
for the identity provider's callback never ran and ConfigureAwait(false) on
it went unnoticed. The sign-out is now answered with the provider sign-out
document and the provider's callback is delivered from another thread, once
the flow is really waiting for it. With ConfigureAwait(false) on that wait
the test fails ("Feedback" raised off the calling thread). The other
operations are answered with the real service's replies.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
CHANGELOG: the unconfirmed sign-in, enrollment and sign-out messages and
when "canceled" is still said; the session discarded after an unconfirmed
hand-over; the refused-operation message; closing the window during an
operation; the two sentences of the pipe identity check, which the
command-line tool and the in-app update show too; the matching log changes.

Threat model: the companion holds a session only while a sign-in attempt
runs and discards it once the service confirms or may hold it.

Also removes the blank line at the end of AgentPipeClient.cs and a fourth
blank line between two tests in ShellStatusTests.cs. (Replayed onto the
release branch, whose coverage minimums are kept as they are; the
presentation assembly's entry at 100 is added by the first commit.)

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
The polling prerelease saved a demand for authorization in the control
state. Nothing in the realtime service sets it; the property was kept,
get-only and always false, only because the companion's footer still read
it. The presentation assembly no longer reads it (ShellStatus.Footer), so
the property goes, with its last mention.

The snapshot's control object therefore no longer carries an
"authorizationRequired" member (it was always false). The runtime test for
a control state saved by the polling prerelease now asserts that instead of
reading the removed property.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
The service can now answer prepare-update with "Not ready for update: ..."
when work claimed before the pause outlasts the preparation limit. The
in-app update showed every reply other than "Ready for update." behind the
same lead, "The service could not prepare for an update. Let active
printing finish and try again.", which is redundant for a not-ready reply
and wrong for an error that has nothing to do with printing.

A not-ready reply is now shown whole, as the service worded it, after "The
update was not started, and the current version remains installed." Any
other reply is shown as an answer the update could not use, without advice
about printing. A runtime test holds the service's real not-ready reply to
that wording.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
- A pipe server that accepts and never reads, then goes away, fails the
  request inside the write. That failure must not be "not sent"; moving the
  mark to after the write now fails this test.
- A server process that cannot be looked up keeps the "not the installed
  service" refusal; treating every failed lookup as an ended process now
  fails a test.
- The reply-limit case no longer uses a 250 ms real limit that also covered
  connect and write. The internal seam takes a clock instead of a duration,
  the test lets the real limits (150 s for enrollment, 35 s otherwise) run
  out on a manual clock after the server has read the request, and checks
  that the client is still waiting one second before.
- The service's sign-in reply uses SignInSentences.SignedIn instead of a
  local copy of the literal.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
The hand-over and what it announces ran inside the region whose failures
mean the sign-in failed, so a view's handler that threw one of the caught
types while "Signed in" was announced was reported and logged as "not
completed" although the service had signed in. The server's part of a
sign-in (password, or the browser exchange) now returns the credential,
and the hand-over runs outside that region. The stage a sign-in ends in is
read from what the flow holds instead of from a result handed back, so it
is "signed in" even when the announcement throws.

Tests for checks that no test held (each mutation now fails one):
- a non-cancellation failure seen after dispose is a quiet end;
- the wait is looked at before the browser is opened, before the
  credential is taken after the browser exchange, and after the server
  described itself;
- the provider's sign-out confirmation is not used once the flow is
  disposed.
The scripted session can now hold the start and the completion of a
browser sign-in. A field in the fixture no longer sits directly above a
documented constant.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
A save that was sent and not answered returned "service unavailable": the
window showed that state, the checkbox went back, and the log said "could
not be saved", although the service may have saved the change.

Such a save is now reported as "Certificate exception could not be
confirmed" with what happened to the request. The flow no longer claims to
know the setting (TrustKnown is false): the window shows the checkbox
undetermined and the label says the last change was not confirmed, and the
flow itself applies no exception meanwhile, so discovery never trusts a
certificate on the strength of a value the service may not hold. The
setting is read again when the window's status refresh gets an answer
(ReadUnconfirmedTrustAsync, which does nothing while the setting is known
or another operation runs); only an answer makes it known again. A save
that never left still returns "unavailable" and puts the checkbox back.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
ShellFeedback.ForAction worded every action without an answer as "Result
could not be confirmed ... may already have reached the server or the
printer", including one the pipe client reports as never sent and one
whose wait had ended before the request was made. A never-sent action now
says "Request not sent. Nothing was sent to the local service, so nothing
was changed", with the reason. An action that left, including one whose
wait the caller ended afterwards, stays unconfirmed. The test that claimed
to cover an abandoned action cancelled before the send; it now cancels
after it, and the never-started case has its own test.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
The snapshot's control object without authorizationRequired and the footer
sentence that went with it; the unconfirmed certificate-exception save; the
"Request not sent" message for an action that never left; and the wording
of an update the service is not ready for.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
The service can also answer prepare-update with "Update preparation was
interrupted: ..." when printing was resumed, the pause ran out, or a
sign-in, sign-out or enrollment began while it waited. Like "Not ready for
update", that is the service's own account of why it cannot be updated now
and already says what to do, so the in-app update shows it whole after "The
update was not started, and the current version remains installed" instead
of as an answer it could not use. A runtime test holds the service's real
reply to that wording.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
james.maes merged commit 7610dec07e into release/2.0 2026-11-10 23:13:47 +00:00
Sign in to join this conversation.