refactor(cli)!: manage the installed service only; remove the legacy mode; cover every line #28

Merged
james.maes merged 13 commits from refactor/2.0-cli-cleanup-on-release-20261110 into release/2.0 2026-11-10 21:52:13 +00:00
Owner

The command line now only manages the installed service over its pipe, every decision in it is tested, and its assembly is gated at 100% line coverage.

Removed

  • The --legacy-compat mode and everything that existed only for it: the separate per-user data directory (--data-dir), the command line's worker lock, the run-once claim pass (run --once), the isolated enroll and expose paths, and --service. It was the last code that sent a device token outside a session.
  • The --printer-worker role: nothing in the product, in any released version, starts the command line as a print helper (the service starts its own executable).
  • The direct reference to Core.

Changed for operators and scripts (exit codes are still only 0 and 1)

  • --help and -h exit 0, alone or after a command; an unknown command or a malformed command line is refused on standard error with a fixed reason and the usage. A refusal never repeats what was typed.
  • Extra arguments, repeated options, blank values and an option name given as a value are refused; nothing is sent.
  • service-status and snapshot exit 1 when the service replies with an error.
  • The enrollment code is prompted for only at a console, where it is not shown while typed; piped input is read as one line with no prompt, so standard error stays empty on success. The code is trimmed, and a leading byte-order mark (Windows PowerShell 5.1 adds one on a UTF-8 console) is dropped.
  • The usage text is new.

Unchanged: the request each command sends to the service, replies printed on standard output, and the two internal roles --apply-update (updater helper) and --auth-callback (browser sign-in hand-over).

Structure: Program.Main is one expression that binds the real console, environment and pipe; CommandLineHost carries those functions; CommandLine holds every decision. 179 test cases, hand-written fakes, plus tests that start the built executable.

Left for a later change (after Core hardening merges): deleting the code in Core and Windows that only the removed mode reached (bearer-token transport and in-memory download in PrintApiClient, the non-options paths of PrintWorker, PrintAsync(PrintJob, byte[]) on the printer backends, the worker host's print operation). The threat model says so.

Verification, local, on this head (rebased onto 43f7de0): build/Verify.ps1 passed twice: 200 Core and 602 Windows/CLI tests, zero warnings; qqq-print-agent 100% (117 of 117 lines; minimum raised from 37 to 100), Core 87.93%, Windows 70.3%.

Not verified: nothing was run against the installed service or its pipe; the no-echo read was driven on a real hidden console by the reviewer, the byte-order-mark case only with simulated input.

Review: independent, three passes. The first found two should-fix items, the second one (the prompt on standard error broke piped enrollment under Windows PowerShell 5.1), the third one (the byte-order mark); all are fixed as the reviewer proposed.

🤖 Generated with Claude Code

The command line now only manages the installed service over its pipe, every decision in it is tested, and its assembly is gated at 100% line coverage. **Removed** - The `--legacy-compat` mode and everything that existed only for it: the separate per-user data directory (`--data-dir`), the command line's worker lock, the run-once claim pass (`run --once`), the isolated enroll and expose paths, and `--service`. It was the last code that sent a device token outside a session. - The `--printer-worker` role: nothing in the product, in any released version, starts the command line as a print helper (the service starts its own executable). - The direct reference to Core. **Changed for operators and scripts** (exit codes are still only 0 and 1) - `--help` and `-h` exit 0, alone or after a command; an unknown command or a malformed command line is refused on standard error with a fixed reason and the usage. A refusal never repeats what was typed. - Extra arguments, repeated options, blank values and an option name given as a value are refused; nothing is sent. - `service-status` and `snapshot` exit 1 when the service replies with an error. - The enrollment code is prompted for only at a console, where it is not shown while typed; piped input is read as one line with no prompt, so standard error stays empty on success. The code is trimmed, and a leading byte-order mark (Windows PowerShell 5.1 adds one on a UTF-8 console) is dropped. - The usage text is new. **Unchanged**: the request each command sends to the service, replies printed on standard output, and the two internal roles `--apply-update` (updater helper) and `--auth-callback` (browser sign-in hand-over). **Structure**: `Program.Main` is one expression that binds the real console, environment and pipe; `CommandLineHost` carries those functions; `CommandLine` holds every decision. 179 test cases, hand-written fakes, plus tests that start the built executable. **Left for a later change** (after Core hardening merges): deleting the code in Core and Windows that only the removed mode reached (bearer-token transport and in-memory download in `PrintApiClient`, the non-options paths of `PrintWorker`, `PrintAsync(PrintJob, byte[])` on the printer backends, the worker host's `print` operation). The threat model says so. **Verification**, local, on this head (rebased onto `43f7de0`): `build/Verify.ps1` passed twice: 200 Core and 602 Windows/CLI tests, zero warnings; `qqq-print-agent` 100% (117 of 117 lines; minimum raised from 37 to 100), Core 87.93%, Windows 70.3%. **Not verified**: nothing was run against the installed service or its pipe; the no-echo read was driven on a real hidden console by the reviewer, the byte-order-mark case only with simulated input. **Review**: independent, three passes. The first found two should-fix items, the second one (the prompt on standard error broke piped enrollment under Windows PowerShell 5.1), the third one (the byte-order mark); all are fixed as the reviewer proposed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
james.maes added 13 commits 2026-11-10 21:52:11 +00:00
The owner decided this for 2.0. The mode talked to servers older than the
realtime printing API, which the service cannot use, and it was the last
code that sent a device token outside a QQQ session.

The command line now only sends requests to the installed service over its
pipe. Gone with the mode: the separate per-user data directory
(--data-dir), the command line's worker lock, the run-once claim pass
(run --once), the isolated enroll and expose paths, and --service, whose
branches inside those paths could never run because the mode refused the
flag. The duplicated enrollment-code read and the hard-coded agent version
"0.1.0" were in the isolated enroll path and go with it.

--legacy-compat, --data-dir, --once and --service are refused by name
before any service call, so a script written for the mode fails instead of
acting on the service. The next change replaces this check with strict
argument parsing.

The executable's three internal roles are untouched: --printer-worker,
--apply-update and --auth-callback. After this change nothing in the
product starts qqq-print-agent.exe as a printer worker (the service starts
its own executable for that; only the removed mode started this one), but
the role still works as before.

Deleted tests, both of the removed mode: ProgramTests
ForegroundData_UsesCurrentUserAndCannotOverwriteServiceCredentials (data
directory selection) and ForegroundMutation_CannotAcquireActiveWorkerDirectory
(worker lock). NativeCliRoutingTests
ForegroundOptionsRequireExplicitCompatibilityBeforeAnyServiceCall is
replaced by RemovedLegacyOptionsAreRefusedBeforeAnyServiceCall.

Code in Core and Windows that only this mode reached in production is left
in place for the branches now editing those projects.

BREAKING CHANGE: --legacy-compat, --data-dir, --once and --service are no
longer accepted.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
The command line's decisions move from Program into CommandLine, which
reaches the outside world only through CommandLineHost: the two output
streams, the environment variable and prompt that supply the enrollment
code, the pipe request sender, and the three helper entry points. Program
is left as one expression that binds the real ones by method reference, so
nothing in the assembly needs the installed service, the real pipe or the
real console to be tested, and nothing is excluded from coverage.

Audit findings fixed here:

- --help and -h exited 1; they now print the usage on standard output and
  exit 0, as help and no arguments already did.
- An unknown command printed the usage on standard output; it now goes to
  standard error with "Unknown command.", still exit 1.
- service-status and snapshot used the real pipe client directly, so no
  test could reach them, and exited 0 even when the service replied
  "Error:". They take the injected sender and exit 1 on an error reply
  like every other command.
- The reply was tested for "Error:" twice; it is examined once.

Arguments are now strict. A command accepts exactly its own options, each
once and each with a non-blank value, in any order; extra words, repeated,
missing or misspelled options are refused before the enrollment code is
read and before the service is contacted. The reason and the usage go to
standard error with exit 1. This replaces the by-name refusal of the
removed legacy options: --legacy-compat, --data-dir, --once and --service
are now simply arguments no command accepts. Until now unknown extra
arguments were ignored.

A refusal never repeats what was typed, because a mistyped command line
can carry an enrollment code or a sign-in callback address and standard
error is often captured.

Unchanged: the request each command sends, the reply printed as the
service wrote it, the sign-out note, the prompt text, exit code 1 for
every failure, and the three internal roles (the printer worker still
runs outside the shared failure handling, because its standard output is
a protocol its parent parses).

ProgramTests and NativeCliRoutingTests are replaced by CliUsageTests,
CliServiceCommandTests and CliArgumentRefusalTests over a hand-written
CliFixture; every behavior the old tests held is held by a new one.

BREAKING CHANGE: a command line with unexpected arguments is refused
instead of having them ignored; service-status and snapshot exit 1 on an
error reply; the argument-error messages changed.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
CliEnrollmentCodeTests: the code is taken from QQQ_PRINT_ENROLLMENT_CODE
without prompting or reading standard input; a missing or blank variable
prompts on standard output and uses the typed line as typed; no code at
all fails with one line on standard error and sends nothing; the code
never appears in standard output or standard error whether enrollment is
accepted, refused by the service, timed out or failed to send; and a code
put on the command line is refused, not used and not repeated.

CliHelperRoleTests: exactly --printer-worker runs the worker, returns its
exit code unchanged, prints nothing and does not catch its failures;
--apply-update passes the request path through and returns the helper's
exit code; --auth-callback forwards the address unchanged with no deadline
of its own and never prints it, including on failure; and one argument too
many or too few starts no helper.

With these the command-line assembly measures 100% line coverage (78 of
78) from tests that run against fakes only. The minimum is raised in the
next change, once the figure has held over repeated full runs.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
CliProcessTests starts the qqq-print-agent.exe built beside the test
assembly and checks what a script would see: --help, -h, help and no
arguments exit 0 with the usage on standard output and nothing on standard
error; an unknown command (the removed --legacy-compat) exits 1 with the
reason and usage on standard error and nothing on standard output.

It also keeps the printer-worker role tested in the shipped entry point.
Started with --printer-worker and no parent, the executable refuses in the
worker's own one-line JSON protocol; started with a live parent, it reads
one request from standard input and answers it. Both requests are ones the
worker must refuse, so no printer is reached. A process that does not end
within a minute is killed and fails the test.

build/minimum-coverage.json: qqq-print-agent 37 -> 100. The assembly
measured exactly 100% (78 of 78 lines) on three consecutive full gate runs
with the minimum already at 100, and on a fourth run in which an unrelated
timing-dependent runtime test failed
(AgentControlConcurrencyTests.RemoteDisconnectAndReconnectDoNotInterruptBlockedPrintOrControlPolling).
Every line is reached by tests that run in-process against fakes, so the
figure does not depend on timing or on the child processes. Nothing in the
assembly is excluded from coverage.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Nothing in the documentation describes --legacy-compat, --data-dir, --once
or --service as usable any more.

- docs/ADMINISTRATION.md gains a Command line section: the commands, where
  the enrollment code comes from, the exit codes and output streams, strict
  arguments, and what to do with per-user state the removed mode left in
  %LOCALAPPDATA%\QqqPrintAgent.
- docs/INSTALLATION.md, docs/QQQ-SIGN-IN.md, docs/DEVELOPMENT.md and
  docs/TROUBLESHOOTING.md lose the legacy diagnostics sentences. Printer
  worker children are started by the service only; the worker.lock rows now
  describe the service, which still holds that lock.
- docs/security/threat-model.md: the command line has no server transport
  and keeps no credentials or journal; a refused command line is never
  echoed.
- CHANGELOG.md records the breaking command-line changes under Unreleased.
- CLAUDE.md: the CLI project row and where the command-line tests live.

Left as dated history: installer/VALIDATION.md, docs/FUNCTIONAL-AUDIT.md,
the completed items in docs/TODO.md and the older CHANGELOG entries.
docs/SESSION-STATE.md is not touched here; its line giving the coverage
minimums as 86 / 68 / 37 is out of date now that the command line is at 100.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Review of 45b448f confirmed that nothing starts qqq-print-agent.exe as a
printer worker. The only worker start is IsolatedPrinterBackend.InvokeAsync,
constructed only by the service with its own executable path, and the same
holds at tags v1.0.1 and v1.2.1; the removed legacy mode was the only code
that ever passed this executable's path.

--printer-worker is therefore no longer a role: the special case in
CommandLine.RunAsync, the host's RunPrinterWorkerAsync and its binding in
Program are gone, and the argument is refused as an unknown command like
any other word. The update helper and the sign-in callback forwarder are
unchanged.

The command line uses no Core type, so the project drops its direct Core
reference and the global using; Core still arrives through the Windows
layer, and the lock file is unchanged. The project file's header comment
described the template, not this project, and is rewritten.

Tests: the worker cases leave CliHelperRoleTests and CliProcessTests;
--printer-worker joins the unknown-command theory, and a process test shows
the real executable writes nothing to standard output when given the
worker's argument and a worker request.

BREAKING CHANGE: qqq-print-agent.exe --printer-worker exits 1 with usage.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
The prompt was written to standard output with no line end, so a script
that piped the code in captured "Enrollment code: Enrolled." as one line,
and a code typed at a console stayed on the screen.

- The prompt goes to standard error and its line is ended there after the
  read, before the reply or an error message. Standard output now holds
  the service's reply and nothing else.
- When standard input is a console the code is read key by key without
  being shown: Enter ends it, Backspace removes the last character, keys
  that produce no printable character are ignored. When standard input is
  redirected a line is read, as before.
- White space around the code is dropped, from the variable and from
  standard input. A code never has any, and cmd's "echo CODE | ..." pipes
  a trailing space. Until now the code was sent as given.

The host gains IsInputRedirected and ReadKey. The key loop lives in
CommandLine, so Backspace and Enter handling is tested with fake keys;
Program binds Console.ReadKey by method reference and a one-line
IsInputRedirected, which an in-process test calls. Nothing is excluded
from coverage. The usage text says how the code is read.

Tests: prompt stream and line end for a piped and a typed code; no key is
ever read with echo; Backspace, an empty-line Backspace and non-character
keys; trimming from all three sources with inner white space kept; no code
at all from a closed, blank or Enter-only input; and a process test that
runs the real executable with the variable removed and standard input
closed, which exercises the real redirected-input binding and stops before
the service would be contacted. The no-echo read was not run against a
real console: that needs an interactive one.

BREAKING CHANGE: the enrollment prompt moved from standard output to
standard error; a typed code is no longer shown; the code is trimmed.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
The usage is a multi-line literal, so its line ends were those of the
source checkout: the built executable wrote bare line feeds inside the
text and one carriage-return pair at the end. The text is now normalized
once, where it is defined.

The process tests compare the real executable's output byte for byte,
line ends included, and a new one requires every line end written to
either stream to be the platform's.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Refusals had one generic reason per command. Each now gets a fixed
sentence built only from the tool's own command and option names, never
from what was typed:

- "'printers' takes no arguments."
- "'expose' accepts only --printer and --exposed."
- "--server is required."
- "--server needs a value."
- "--printer may be given only once."
- "--exposed must be true or false."
- "The options --service, --legacy-compat, --data-dir and --once were
  removed in 2.0: the command line only manages the installed service."
  when any of the four appears anywhere, so a 1.x script learns why it
  fails.
- "Unknown command." as before.

A value that begins with "--" is refused as a missing value. Until now
"enroll --server --server" sent the server "--server", and
"expose --printer --exposed --exposed true" sent the printer "--exposed".

A help flag in last place after a real command (printers --help,
enroll --server URL -h) prints the usage on standard output and exits 0
instead of being refused. After an unknown word, or in the middle of a
command line, it is still an ordinary argument.

--exposed accepts exactly the word true or false in any letter case.
Padded values (" true") were accepted by bool.TryParse and are now
refused.

The usage text says the help flags also work after a command.

Tests: CliArgumentRefusalTests asserts the exact reason for every
malformed combination, the removed-option sentence for every placement of
the four options, the trailing help flag, and that a refusal reads no
variable, no line and no key. Usage_DoesNotMentionTheRemovedLegacyMode is
removed: the pinned usage text already holds that.

BREAKING CHANGE: refusal messages changed; "<command> --help" exits 0;
padded true/false is refused; an option name is not accepted as a value.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
- CHANGELOG.md: the command-line entry is now a list of what an operator
  or script can notice: the removed mode and the sentence its options get,
  the printer-worker role gone, help after a command, the new usage text,
  refusal wording with the usage, strict arguments, the enrollment prompt
  on standard error without echo, trimming, and the Error: exit code.
- docs/ADMINISTRATION.md: a service "Error:" reply is printed on standard
  output with exit code 1, and standard error carries only the command
  line's own failures, the prompt and refusals; how the enrollment code is
  read; which values --exposed takes; the refusal reasons; help after a
  command; --printer-worker.
- docs/security/threat-model.md: the device-token sentence now says no
  path the shipped programs can reach sends it outside a session; the
  bearer constructor still ships in Core until it is deleted. The
  enrollment code arrives by variable or standard input, never echoed.
- docs/FUNCTIONAL-AUDIT.md: a dated note under the table for the three
  rows that describe the removed foreground mode and cite deleted tests.
  The rows are left as written.
- CONTRIBUTING.md and docs/adr/0003-version-2.md: an assembly that has
  reached its release bar is recorded at the bar, which is why the command
  line is at 100.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
The prompt was written to standard error whenever the variable was not
set, including when the code was piped in. Windows PowerShell 5.1 turns
any standard error output of a native program into an error record, and
with ErrorActionPreference Stop and standard error redirected the line
"Enrollment code: " ended the script as a NativeCommandError, after the
single-use code had been sent.

- Redirected input is read as one line with no prompt and no line end.
  A scripted enrollment that succeeds, by pipe or by the variable, writes
  nothing to standard error. With no code available, the reason is the
  only line there.
- At a console the prompt is still written to standard error, and its
  line is now ended in a finally, so a failure while reading keys starts
  its message on a new line.
- The key-by-key read also ends on a key that carries a carriage return
  or line feed without being the Enter key (Ctrl+M, or a line end a
  terminal delivers as a bare character). It used to keep waiting.

The usage text says the code is prompted for only at a Windows console,
that redirected input is read without a prompt, and that an unattended
caller must set the variable or redirect input.

Tests: a successful piped and a successful variable enrollment write
nothing to standard error; the piped and no-code cases expect no prompt;
the typed line ends on Ctrl+M and on bare carriage return and line feed
keys; a key-read failure follows an ended prompt line. The process test
with closed input now expects the reason alone, which also excludes a
binding that mistakes redirected input for a console.
InputRedirectionBinding_ReportsTheConsoleState says in its summary that
it only executes the binding and names the tests that prove each branch.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
docs/ADMINISTRATION.md, docs/security/threat-model.md and the CHANGELOG
entry now say:

- Redirected input is read without a prompt, and a successful scripted
  enrollment writes nothing to standard error.
- "Not shown" holds at a Windows console. A terminal that gives native
  programs a pipe instead of a console (the default Git Bash window) is
  read as redirected input, and the terminal may echo what is typed; use
  the variable there.
- An unattended caller that has a console nobody can type at (a scheduled
  task) must set the variable or redirect input, or the command waits.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Windows PowerShell 5.1 on a machine whose console code page is UTF-8 puts
a byte-order mark in front of text it pipes to a native program. The line
read keeps it as U+FEFF, which is not white space, so trimming left it in
place, the blank check passed, and the service was sent a code with a
leading mark that the server would reject as unknown.

The code from every source is now trimmed, stripped of a leading U+FEFF
and trimmed again. A variable or an input line that holds only a mark and
white space counts as holding no code.

Tests: marked codes from a pipe, the variable and the console are sent
without the mark; mark-only sources fail as "Enrollment code is required."
and send nothing. The mark is written as an escape in the sources.

The comment, the administration guide and a test summary said Windows
PowerShell treats any standard error output of a native program as an
error. It does so when standard error is redirected or captured (2>&1, a
file, the ISE, remoting); they now say that. The guide and the changelog
mention the mark.

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