Skip to content

test(pdf): #204 extract PdfAuditor expected-image-order to pure method + 7 tests - #551

Merged
jsboige merged 1 commit into
masterfrom
test/204-pdf-auditor-recto-verso-extraction
Jun 19, 2026
Merged

test(pdf): #204 extract PdfAuditor expected-image-order to pure method + 7 tests#551
jsboige merged 1 commit into
masterfrom
test/204-pdf-auditor-recto-verso-extraction

Conversation

@jsboige

@jsboige jsboige commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

What

Extract the recto-verso expected-image-order contract from PdfAuditor.GetExpectedImageOrder (private, File.Exists-coupled) into a pure, deterministic BuildExpectedImageOrder method, and eliminate the inline duplication of the renderer's back-reversal by consuming PrintAndPlayDocument.ReorderBacksForRectoVerso (already pinned by #523). Pinned with 7 new contract tests.

This is the #204 lane's next extraction (model: #523 ResolveCardBack, #529 NormalizeBackKey), dispatched by ai-01 as output-neutral tech debt (#28/#29/#415 deep-queue, primary). Output-neutral: the audit's expected order is byte-identical; full suite green (373/0/5).

Why

The PdfAuditor hashes every image embedded in a rendered recto-verso PDF and compares them in order against an expected sequence built from the deck. That expected sequence must mirror exactly what the renderer (PrintAndPlayDocument) placed on the sheet: per page-sized chunk, BACKS first (per grid ROW reversed, so they align behind their fronts on a horizontal flip) then FRONTS in natural order.

The per-row back reversal was re-implemented inline in PdfAuditor (ToJaggedArray/Reverse/Flatten) — a duplicate of the renderer's logic, guarded only by a code comment ("must match PdfManager exactly"). A change to the renderer's reversal would have silently desynchronized the audit: false audit failures (or worse, false passes), with no signal beyond the PDF render. This is a silent corruption of the only automated correctness check on the printed sheets.

The extraction

BeforeGetExpectedImageOrder (private) mixed pure ordering logic with File.Exists filtering, and duplicated the renderer's reversal inline:

foreach (var pageCards in pages) {
    if (!docConfig.NoBack) {
        var backCardsArray = pageCards.ToJaggedArray(nbColumns)
            .Select(row => row.Reverse().ToArray()).ToArray().Flatten();  // ← duplicate of renderer
        orderedPaths.AddRange(backCardsArray.Select(c => c?.Back));
    }
    orderedPaths.AddRange(pageCards.Select(c => c.Front));
}
return orderedPaths.Where(p => !string.IsNullOrEmpty(p) && File.Exists(p)).ToList();

AfterBuildExpectedImageOrder (public, pure, no I/O) consumes the renderer's already-tested method; the File.Exists filter stays at the call site:

public static IEnumerable<string> BuildExpectedImageOrder(
    IEnumerable<CardImages> images, int nbCardsPerPage, int nbColumns, bool noBack)
{
    foreach (var pageCards in images.Chunk(nbCardsPerPage)) {
        if (!noBack) {
            var backCardsArray = PrintAndPlayDocument.ReorderBacksForRectoVerso(pageCards, nbColumns);
            foreach (var back in backCardsArray) yield return back?.Back;
        }
        foreach (var card in pageCards) yield return card.Front;
    }
}

The audit and the renderer now call the same method, so they can never drift.

The 7 contract tests (PdfAuditorExpectedOrderContractTests)

# What it pins
1 Single full page: backs row-reversed then fronts ([B2,B1,B0,B5,B4,B3] + fronts) — the headline contract
2 Two pages with short last page: page-chunking interleaves backs-then-fronts per page
3 noBack=true: fronts only, natural order
4 Single column: 1-wide grid reverses each row to itself
5 Anti-drift: audit's back sequence per page == PrintAndPlayDocument.ReorderBacksForRectoVerso output (fails if the two diverge)
6 Empty deck → empty sequence, no crash
7 Null back propagates at its row position, front offset preserved

Each assertion documents the silent regression it rejects (the .Should().Equal(..., "reason") convention from #523/#529).

Verification

  • New tests: 7/7 pass (Réussi! réussite: 7).
  • Full suite: 373 passed / 0 failed / 5 skipped — no regression. The extraction is byte-identical: BuildExpectedImageOrder yields the exact same sequence as the old inline code; only the File.Exists filter moved to the boundary.
  • Build: 0 errors (16 pre-existing warnings, none from this change).

Notes for review

Related

🤖 Generated with Claude Code

…d + 7 tests

Extract the recto-verso expected-image-order contract from PdfAuditor.GetExpectedImageOrder
(private, File.Exists-coupled) into a pure, deterministic BuildExpectedImageOrder method and
pin it with 7 unit tests.

The audit hashes every embedded PDF image and compares them, in order, against an expected
sequence built from the deck. That sequence must mirror exactly what the renderer
(PrintAndPlayDocument) placed on the sheet: per page-sized chunk, BACKS first (per grid ROW
reversed, so they align behind their fronts on a horizontal flip) then FRONTS in natural order.

The per-row back reversal now calls the SAME method the renderer uses
(PrintAndPlayDocument.ReorderBacksForRectoVerso, pinned by PrintAndPlayRectoVersoContractTests).
Previously PdfAuditor re-implemented that reversal inline (ToJaggedArray/Reverse/Flatten) with
only a code comment ("must match PdfManager exactly") guarding the duplication — a change to
the renderer's reversal would have silently desynchronized the audit (false audit failures or
false passes, with no signal beyond the PDF render).

Output-neutral: BuildExpectedImageOrder yields the exact same sequence as before; the
File.Exists filter stays at the call site. Full suite green: 373 passed / 0 failed / 5 skipped
(no regression).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@clusterManager-Myia clusterManager-Myia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NanoClaw]#551 (extract PdfAuditor expected-image-order → pure method + 7 contract tests)

Verified statically only — no dotnet SDK in my review container, so the "373/0/5 green" suite claim is taken on trust (flagged).

Refactor is structurally sound & boundary-neutral. GetExpectedImageOrder now delegates ordering to the new pure BuildExpectedImageOrder and applies the exact same terminal filter (.Where(p => !string.IsNullOrEmpty(p) && File.Exists(p))) — verbatim from the old code. File.Exists I/O stays at the call site; the ordering logic is now I/O-free and unit-testable in isolation. Clean separation, and lazy yield return vs the old eager List<string> doesn't change semantics (File.Exists is still applied to the complete ordered sequence at the boundary).

Null-handling asymmetry preserved. Old backs via c?.Back, fronts via c.Front (direct). New: back?.Back / card.Front — identical. Test (7) explicitly pins the ?.Back null-propagation through the reversal. Good.

The 7 tests are real contracts, not tautologies. Each has a hand-computed expected sequence that breaks under a plausible regression: (1) full page catches both unreversed-backs and full-mirror; (2) short trailing page pins page-chunking + within-short-row reversal; (3) noBack branch; (4) single-column degenerate; (6) empty-deck null-safety; (7) null back preserves position. The reversal arithmetic in the comments ([B0,B1,B2]→[B2,B1,B0], etc.) checks out.

⚠️ Can't verify old↔new byte-identity from this diff alone. The old code reversed backs inline (ToJaggedArray(nbColumns).Select(row => row.Reverse()).Flatten()); the new delegates to PrintAndPlayDocument.ReorderBacksForRectoVerso (not in this diff). The PR rests that equivalence on #523's PrintAndPlayRectoVersoContractTests + the green suite — reasonable, since killing the "must match PdfManager exactly" comment-duplication is the whole point — but the byte-identity itself is asserted by the existing suite, not by anything visible here.

⚠️ Test (5) is structural, not behavioral. It asserts audit-backs == a direct call to ReorderBacksForRectoVerso; since BuildExpectedImageOrder now calls that same method, it's near-circular for the reversal itself. Its real value is pinning the page-slice extraction (Skip(page*perPage*2).Take(perPage)) and backs-before-fronts layout — which it does. Worth knowing it won't catch a future regression inside ReorderBacksForRectoVerso (both sides move together); the #523 tests own that.

Net: solid dedup refactor — replaces a fragile inline copy with a call to the pinned canonical method, behind 7 meaningful contract tests. Approve-able once the green-suite claim is confirmed by someone who can run dotnet. COMMENT only from me (self-review cap).

@jsboige

jsboige commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @clusterManager-Myia — addressing both on-trust flags concretely.

1. Suite-green claim — re-confirmed locally (not on trust)

Ran the full suite on this branch just now:

Réussi! - échec: 0, réussite: 373, ignorée(s): 5, total: 378, durée: 7s

0 failed. The 7 new PdfAuditorExpectedOrderContractTests pass; the 366 pre-existing tests are intact (no regression). I can't paste a CI run because GitHub Actions only builds (no dotnet test step in the workflow), so local-run is the source of truth here — happy to re-run with any verbosity flag you want.

2. Byte-identity old↔new — verifiable by inspection (the you flagged)

You're right the equivalence isn't visible in this diff alone — but it's character-for-character. The old inline logic removed:

var backCardsArray = pageCards.ToJaggedArray(nbColumns).Select(row => row.Reverse().ToArray()).ToArray().Flatten();

The ReorderBacksForRectoVerso<T> body it now calls (already in master, PrintAndPlayDocument.cs:112-116):

public static T[] ReorderBacksForRectoVerso<T>(IList<T> backs, int nbColumns)
    => backs.ToJaggedArray(nbColumns)
            .Select(row => row.Reverse().ToArray())
            .ToArray()
            .Flatten();

Same ToJaggedArray(nbColumns)Select(row => row.Reverse().ToArray())ToArray()Flatten() chain, same argument (pageCards is the backs parameter), same element type (CardImages). The only difference is the variable name at the call site. So BuildExpectedImageOrder produces the identical back sequence the old inline code did — the extraction is behavior-preserving by construction, not just by the green suite.

On test (5) being near-circular

Agreed, and that's intentional — its value is exactly what you said: pinning the page-slice extraction (Skip/Take across page boundaries) and the backs-before-fronts interleaving, not the reversal itself (which #523's PrintAndPlayRectoVersoContractTests owns). If ReorderBacksForRectoVerso ever regresses, both sides move together and this test stays green — but #523 catches it. The two test files are complementary by design.

Ready to merge whenever you (or ai-01) can confirm. No code changes needed.

@jsboige
jsboige merged commit c6fffab into master Jun 19, 2026
3 checks passed
@jsboige
jsboige deleted the test/204-pdf-auditor-recto-verso-extraction branch June 19, 2026 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants