propose fix for tonersave queue default

This commit is contained in:
juergen committed 2026-10-20 15:42:46 +02:00
1 parent f556103f3c
commit 0b1276843f
5 files changed
+328

No files matched your search

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-20
@@ -0,0 +1,130 @@
# Design
## Context
See proposal.md — Why for the observed split. The pieces that shape the
approach:
- `entrypoint.sh` (`apply_printer_options`, lines ~341–383) validates every
`key=value` against `lpoptions -p <queue> -l` and then calls
`lpadmin -p <queue> -o <opt>`, logging `queue default <opt> applied` on exit
status 0. Exit status is the only signal it has, and `lpadmin` returns 0 even
when the setting does not take effect.
- The queue's settings are persisted in the queue's PPD copy
`/etc/cups/ppd/<queue>.ppd` (`*DefaultTonerSaveMode: True`,
`*DefaultBRColorMode: None`), not as `printers.conf` `Default*` lines; with
this CUPS the settings file alone therefore cannot distinguish "applied" from
"accepted and dropped".
- Confirmed in two containers (manual `lpadmin -o` and the entrypoint's own
startup batch): after `lpadmin -o TonerSaveMode=True` the PPD copy says
`True`, while `lpoptions -p <queue> -l` keeps showing `TonerSaveMode/Toner
Save: *False True`; `BRColorMode` in the same batch is consistent
(PPD copy `None`, `-l` `*None`).
- `TonerSaveMode` is a Boolean PPD option (assets/.../BR4150_2.PPD line 327)
whose `True` branch emits `statusdict/tonersave known{statusdict begin true
tonersave end}if`; the six `APPrinterPreset` entries set
`*TonerSaveMode True|False` for draft/normal/high, which is how the
documented driverless draft behaviour is implemented.
- The rejected observation point: `lpoptions -p <queue>` without `-l` lists
only IPP queue attributes, so it never shows PPD option defaults. The
archived `fix-colour-defaults-and-client-options` verification used
`lpoptions -p <queue> -l` (correct), and the `revert-colour-mode-default`
task 3.2 command was corrected to match during apply.
- The repo has no test suite yet; the job-level check introduced here is a
self-contained script that drives `podman`, a placeholder `PRINTER_IP` and a
minimal hand-written PostScript/PDF sample (no sample fixtures exist in the
repo), so it stays runnable without a printer and without network access to a
real device.
## Goals / Non-Goals
**Goals:**
- Determine whether a job actually receives an applied `TonerSaveMode` queue
default, from the emitted PostScript, not from settings files.
- Turn the runtime-queue-default requirement into one that a job-level test can
falsify, so the same class of "accepted but ineffective" defect cannot pass
again.
- Leave the spec honest: a verified behaviour becomes the scenario; a verified
limitation is recorded instead of an aspiration.
**Non-Goals:**
- Changing `entrypoint.sh` validation/plumbing unless the investigation
identifies a fixable server-side cause.
- Re-testing the colour-saturation limitation (documented by
`revert-colour-mode-default`) or re-opening `BRColorMode` behaviour, which is
verified.
- Adding job-level assertions for every PPD option; the example in the
requirement (`BRColorMode` + `TonerSaveMode`) is the coverage.
## Decisions
**D1 — Observation point for queue defaults: `lpoptions -p <queue> -l`
(asterisks) plus the queue PPD copy; never `lpoptions -p` without `-l`.**
Rationale: `-p` without `-l` shows IPP queue attributes only (see Context) and
produced a false negative during `revert-colour-mode-default`. `-l` reflects the
option the queue would use; the PPD copy shows what was persisted. Both are
still *configuration* views, so they are used only to set up and cross-check a
scenario, never as the acceptance signal.
Alternatives considered: `printers.conf` `Default*` lines (absent with this
CUPS — rejected); the CUPS web admin page (same settings source, needs auth —
rejected); `lpstat -l -p` (no option detail — rejected).
**D2 — Acceptance signal is the emitted PostScript spooled by cupsd.**
Rationale: it is the only artefact that shows what the printer receives, it
covers presets, queue defaults and per-job options in one channel, and the
existing spec already reasons in those terms (`<</UseCIEColor false>>`). The
spool file is the last file in `/var/spool/cups/d<JobId>-*` (its name comes from
the preceding `lpstat -W completed -o` line), and is readable as root inside the
container for a few minutes while the socket connection to the placeholder
`PRINTER_IP` fails.
Alternatives considered: asserting on `lpoptions -l` alone (that is exactly the
blind spot being fixed — rejected as the acceptance signal, kept as setup
cross-check); parsing the *source* `/usr/share/cups/model/BR4150_2.PPD` in the
image (proves packaging, not the job — rejected); a throwaway CUPS-side hook or
debug logging (more moving parts than one file read — rejected).
**D3 — Investigate before specifying the `TonerSaveMode` outcome.**
Rationale: the change must not encode an unverified assumption. Task group 1
runs the probe and records the result (spool PostScript, `-l` state, and — for a
second data point — an explicit per-job `-o BRColorMode=Vivid` in the same job
that must visibly differ from the queue default `None`). If the queue default
does reach the job, the scenario as written stands. If it does not, the
investigation result is added to this design and the scenario is reworded to the
verified behaviour plus an explicit limitation note before the checkboxes for
task group 2/3 are ticked.
Alternatives considered: writing the scenario as aspirational and letting the
test fail until someone fixes it (leaves a red suite and an untrue spec —
rejected); downgrading the scenario up front to "the PPD copy is written"
(keeps the blind spot — rejected).
**D4 — Prefer a PostScript-text assertion over a rendered/printed test.**
Rationale: it is deterministic, needs no printer, and is CI-friendly. A physical
print (toner save visibly lighter) stays a human check and is not part of the
automation.
Alternatives considered: rasterising the output and comparing image statistics
(flaky, tooling-heavy — rejected).
**D5 — `TonerSaveMode` is compared against the vendor `False` default, not the
preset wiring.** Rationale: the requirement asserts the *queue default* path;
whether a client preset overrides it is the separate, already documented
driverless scenario. Both must remain visible in the emitted PostScript, which
the test checks by applying the documented `PRINTER_OPTIONS` example.
## Risks / Trade-offs
- The spool file is lifetime-bounded (retention) and the test container's job
never reaches a printer, so the test must read the file soon after submission
and tolerate `connection refused` state messages; if it races, the preferred
per-job capture is `cupsfilter <sample.pdf>` inside the container with the
same PPD, which produces the same PostScript text without a queue.
- `lpadmin -o` behaviour is option- and CUPS-version-specific; the test must
state the CUPS version it was verified against so a future base-image bump
does not silently reinterpret the assertion.
- If the investigation finds the drop is a CUPS/driverd quirk with no image-side
fix, the change still lands value (honest spec + regression guard) but leaves
`TonerSaveMode` as a documented limitation; that outcome must be reported, not
absorbed.
- Boolean `TonerSaveMode` is the same option the draft presets tune, so a future
preset change can mask a broken queue default; the job-level test deliberately
submits a job *without* `print-quality`/preset attributes to keep the two
paths separate.
@@ -0,0 +1,80 @@
# Proposal
## Why
While verifying the `revert-colour-mode-default` change, an operational check
found a divergence for one PPD option: after the entrypoint applies
`PRINTER_OPTIONS="BRColorMode=None TonerSaveMode=True"`, the queue shows
`BRColorMode ... *None` (consistent everywhere) but `TonerSaveMode ... *False`
even though the queue's PPD copy at `/etc/cups/ppd/<queue>.ppd` contains
`*DefaultTonerSaveMode: True`. The rejected tool option
`lpadmin -p <queue> -o TonerSaveMode=True` (config-only, immediately surfaced)
reproduces the same split. `TonerSaveMode` is the option that the
`APPrinterPreset` entries use to implement the "draft prints with toner saving"
behaviour recorded in the spec, so a queue default that only half applies
undermines one of the documented scenarios.
The current spec (`Queue defaults are configurable at runtime` → *Options are
applied on start*) is written as a configuration check ("the queue defaults
include `TonerSaveMode=False`") and is satisfied by the settings file alone, so
it does not pin down whether a job actually receives `TonerSaveMode=True`. That
gap is why the divergence went unnoticed. This change turns the requirement
into an observable one — the emitted job — and makes whatever the investigation
finds explicit in the spec instead of leaving the contradiction undocumented.
## What Changes
- **Investigate** the `TonerSaveMode` split during apply: determine whether a
job with the queue default `TonerSaveMode=True` actually emits the toner-save
PostScript (`statusdict/tonersave` → `true`) or silently falls back to the
PPD default `False`, and record the root cause in the design/tasks notes.
- **Spec** (`printserver/cups`): replace the configuration-only *Options are
applied on start* scenario with an observable job-level scenario; add a
scenario that pins down what a job whose queue default conflicts with the PPD
default must emit. Add the `TonerSaveMode` observable to the colour-mode
requirement's scope (BRColorMode only) so the two are not conflated.
- **Tests** (`tests/`): add a job-level integration test that applies the
documented `PRINTER_OPTIONS="BRColorMode=Vivid TonerSaveMode=False"` example,
submits a job with the bundled sample, and asserts the emitted PostScript
carries `UseCIEColor true` (Vivid) **and** `statusdict ... tonersave ... false`
(TonerSaveMode) — i.e. the queue default wins over the PPD default in the
output. If the investigation shows it cannot win, the test instead asserts the
honest current behaviour and the spec records the limitation.
- **README**: state what `PRINTER_OPTIONS` queue defaults do to a job, with the
verified example output, and note any option-specific caveat found.
No change to `entrypoint.sh` plumbing is planned: it already calls
`lpadmin -o <option>` for every validated `key=value` pair, and its warning
path for unusable entries stays as is. A code change happens only if the
investigation identifies a server-side cause that is fixable in the image.
## Capabilities
### New Capabilities
(none)
### Modified Capabilities
- `printserver/cups`: the runtime queue-default requirement becomes
job-observable, and a scenario covers a queue default that conflicts with the
PPD default.
## Impact
- `openspec/specs/printserver/cups/spec.md` (requirement text at sync time;
the delta lives in this change).
- **No project test suite exists yet** (AGENTS.md: no build/test/lint config) —
this change introduces the first one, a self-contained
`tests/test-queue-defaults.sh` that drives `podman`/`podman exec`, so the
verification is reproducible instead of an ad-hoc shell session.
- `README.md` — `PRINTER_OPTIONS` documentation gains the verified job-level
behaviour.
- `assets/Brother HL-4150CDN/ppd/BR4150_2.PPD` — the six `APPrinterPreset`
entries that set `*TonerSaveMode` are the client that exercises the option;
they stay unchanged.
- `entrypoint.sh` — unchanged unless the investigation finds a server-side
cause.
- Out of scope: the colour saturation limitation documented by
`revert-colour-mode-default` (no server-side lever is known, and none is
proposed here).
@@ -0,0 +1,33 @@
# Spec Delta
## MODIFIED Requirements
### Requirement: Queue defaults are configurable at runtime
The container SHALL accept a `PRINTER_OPTIONS` environment variable holding
space-separated PPD option assignments (`key=value`) and apply each assignment
as a queue default on every start, so PPD-specific device options survive the
recreation of the queue. An applied assignment SHALL also determine what a job
prints: a job that does not select that option itself is printed with the
configured value, even when that value differs from the PPD's own default. A
queue default SHALL be observable for both the colour mode (`BRColorMode`) and
the toner-saving switch (`TonerSaveMode`).
#### Scenario: Options are applied on start
- **WHEN** the container is started with `PRINTER_OPTIONS="BRColorMode=Vivid TonerSaveMode=False"`
- **THEN** after startup the queue defaults include `BRColorMode=Vivid` and `TonerSaveMode=False`
#### Scenario: An applied queue default reaches the job
- **WHEN** the container is started with `PRINTER_OPTIONS="BRColorMode=Vivid TonerSaveMode=False"` and a colour job is submitted that selects neither option
- **THEN** the emitted PostScript applies `Vivid` (`UseCIEColor true`) and, provided the investigation confirms the job-level path, disables toner saving (`statusdict ... tonersave ... false`) — for `TonerSaveMode` the vendor PPD default is `False`, so the observable difference is between omitting the toner-save call and emitting `false`
#### Scenario: Defaults apply to driverless clients
- **WHEN** a macOS or iOS client prints a job that does not specify that PPD option
- **THEN** the job is printed with the configured queue default
#### Scenario: An unusable option must not stop startup
- **WHEN** `PRINTER_OPTIONS` contains an option name or value the PPD does not provide
- **THEN** the entrypoint logs a warning, applies the remaining assignments and continues starting
#### Scenario: Nothing configured
- **WHEN** `PRINTER_OPTIONS` is unset or empty
- **THEN** the queue keeps the PPD defaults and the container starts as before
@@ -0,0 +1,83 @@
# Tasks
Aufteilung: 1. Ursache feststellen (blockierend), 2. Testschicht, 3. Spec/README,
4. Serverseitiger Fix nur falls 1 ihn hergibt.
## 1. Ursache feststellen (Verifikation des Ist-Zustands)
- [ ] 1.1 Jobpfad reproduzieren: Image bauen, Testcontainer mit
`PRINTER_OPTIONS="BRColorMode=None TonerSaveMode=True"` starten, einen Job
einreichen (`podman exec <c> lp -d <queue> -o raw <sample>` oder
`cupsfilter <sample>`), und im versandten PostScript
(`/var/spool/cups/d<JobId>-*`, Job-Nummer aus
`lpstat -W completed -o`) prüfen, ob der Tonerspar-Zweig
(`statusdict/tonersave` … `true`) enthalten ist.
Verifikation: Ergebnis (enthalten/nicht enthalten) mit dem ausgelesenen
Spool-Pfad und der CUPS-Version in design.md notieren.
- [ ] 1.2 Zweiten Messpunkt gegen die Queue-Defaults aufnehmen: denselben Job
zusätzlich mit explizitem `-o BRColorMode=Vivid` einreichen und die
`UseCIEColor`-Zeile im Spool mit der des ersten Jobs vergleichen.
Verifikation: dokumentierter Unterschied (oder dessen Fehlen) zeigt, ob
per-Job-Optionen im selben Pfad sichtbar werden — belegt, dass die
Messmethode aus 1.1 trägt.
- [ ] 1.3 Queue-Zustand desselben Containers gegengelesen:
`lpoptions -p <queue> -l` (Sternchen) und
`grep '^Default' /etc/cups/ppd/<queue>.ppd`.
Verifikation: die Abweichung aus dem Ausgangsbefund ist entweder
bestätigt (`*False` bei `DefaultTonerSaveMode True`) oder widerlegt;
Ergebnis in design.md festhalten.
- [ ] 1.4 Ergebnis einordnen und die Entscheidung nach Design D3 treffen:
erreicht der Queue-Default den Job (Szenario in specs/… bleibt) oder nicht
(design.md um das Resultat ergänzen, Szenario auf das verifizierte
Verhalten umformulieren, Limitation explizit benennen).
Verifikation: design.md enthält den Messwert und die getroffene
Entscheidung; die betroffenen Tasks aus 2/3 sind entsprechend markiert.
Ohne dieses Ergebnis werden 2.x/3.x nicht abgehakt.
## 2. Automatisierter Nachweis (neue Testschicht)
- [ ] 2.1 `tests/test-queue-defaults.sh` anlegen: baut das Image, startet einen
Container mit `PRINTER_OPTIONS="BRColorMode=Vivid TonerSaveMode=False"`,
wartet auf `ready: CUPS on 631/tcp`, reicht einen Job ohne
`print-quality`/Preset-Attribute ein und liest das letzte Spool-File
(`lpstat -W completed -o` → `/var/spool/cups/d<JobId>-*`).
Verifikation: Skript läuft lokal durch (`bash tests/test-queue-defaults.sh`),
Ausgabe zeigt den Spool-Pfad und die gefundenen PostScript-Zeilen.
- [ ] 2.2 Assertions gemäß Ergebnis aus 1.4 einbauen: `UseCIEColor true` für den
Queue-Default `Vivid` und der Tonerspar-Zustand aus dem verifizierten
Verhalten für `TonerSaveMode=False`; zusätzlich muss die
`UseCIEColor`-Zeile bei explizitem `-o BRColorMode=None` abweichen
(Regressionsschutz). Vergleich nur über `grep -F` auf Zeilen, kein Parsen
von PDF/PS-Struktur.
Verifikation: Skript endet mit Exit 0 und einer PASS-Zeile pro Assertion;
eine absichtlich falsch gesetzte Erwartung lässt es mit Exit ≠ 0 und
Klartextzeile scheitern.
- [ ] 2.3 Lebensdauer/Umgebung absichern: Container immer aufräumen (Trap),
kurze Wiederholung des Spool-Lesens statt Polling, und im Skriptkopf die
verifizierte CUPS-Version notieren.
Verifikation: `podman ps -a` zeigt nach einem Lauf keinen Testcontainer;
zweiter Lauf hintereinander ist grün.
## 3. Spec und README angleichen
- [ ] 3.1 `README.md`, Abschnitt *Environment variables* / `PRINTER_OPTIONS`:
eine Zeile ergänzen, was ein Queue-Default für den Ausdruck bedeutet
(Queue-Default schlägt den PPD-Default, sichtbar im versandten
PostScript), mit Verweis auf das Testskript.
Verifikation: `grep -n 'PRINTER_OPTIONS' README.md` zeigt den neuen Satz;
die genannte Assertion stimmt mit 2.2 überein.
- [ ] 3.2 `entrypoint.sh`-Hinweis prüfen: der bestehende Kommentar bei
`apply_printer_options` (Zeile ~341–345) behauptet, `lpadmin` würde
unbekannte Zuweisungen speichern — mit dem Befund aus 1.x abgleichen und
nur bei Bedarf präzisieren.
Verifikation: `jj diff` zeigt entweder keine Änderung an entrypoint.sh
oder ausschließlich Kommentar/Log-Text, ohne Verhaltensänderung.
## 4. Aktualisierung der Hauptspec (Archive-Zeitpunkt)
- [ ] 4.1 Beim Archivieren die Delta-Spec nach
`openspec/specs/printserver/cups/spec.md` übernehmen und dabei die alte
Fassung des Requirements *Queue defaults are configurable at runtime*
(Zeilen 69–89) durch die neue, job-beobachtbare Fassung ersetzen.
Verifikation: `openspec validate --strict` ohne Findings; die Hauptspec
enthält genau eine Fassung des Requirements.