Skip to content

test(pdf): #204 extract PdfManager filename-suffix insertion to pure method + 6 tests - #553

Merged
jsboige merged 1 commit into
masterfrom
test/204-pdfmanager-filename-suffix-extraction
Jun 19, 2026
Merged

test(pdf): #204 extract PdfManager filename-suffix insertion to pure method + 6 tests#553
jsboige merged 1 commit into
masterfrom
test/204-pdfmanager-filename-suffix-extraction

Conversation

@jsboige

@jsboige jsboige commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

What

Extract the per-back / FacesOnly PDF filename-suffix contract from PdfManager.GenerateBackFirstOneDocPerBack (inlined three times: a LastIndexOf('.') split computed once, then two Substring interpolations) into a pure, deterministic InsertSuffixBeforeExtension method, and pin it with 6 contract tests (8 executions incl. a 3-case Theory).

This is the #204 lane's next extraction (model: #523 ResolveCardBack, #529 NormalizeBackKey, #551 BuildExpectedImageOrder), continuing the output-neutral tech-debt dispatch. Output-neutral: the call sites produce the exact same filenames; full suite green (374/0/5).

Why

GenerateBackFirstOneDocPerBack emits one PDF per distinct back art, plus an extra -FacesOnly PDF for back-less cards. Each filename takes the deck's base name and inserts a suffix just before its final dot: Cards.pdf + "-1"Cards-1.pdf; Cards.pdf + "-FacesOnly"Cards-FacesOnly.pdf.

This logic was inlined three times in a method already flagged by a BUGFIX CORRIGÉ comment (the back/no-back partition was previously buggy). A regression here — inserting at the FIRST dot, dropping the extension, an off-by-one on the 1-based counter, or a naive switch to Path.GetFileNameWithoutExtension — silently produces wrongly-named PDFs that overwrite each other or land in the wrong slot, caught only by inspecting the output directory.

The extraction

Before — three inline occurrences sharing one indexInsert:

var indexInsert = baseName.LastIndexOf('.');
// ... later, in a loop:
var newName = $"{baseName.Substring(0, indexInsert)}-{backIndex + 1}{baseName.Substring(indexInsert)}";
// ... and:
var facesOnlyName = $"{baseName.Substring(0, indexInsert)}-FacesOnly{baseName.Substring(indexInsert)}";

After — one pure method, two call sites:

public static string InsertSuffixBeforeExtension(string baseFileName, string suffix)
{
    var indexInsert = baseFileName.LastIndexOf('.');
    return baseFileName.Substring(0, indexInsert) + suffix + baseFileName.Substring(indexInsert);
}
// call sites:
var newName = InsertSuffixBeforeExtension(baseName, $"-{backIndex + 1}");
var facesOnlyName = InsertSuffixBeforeExtension(baseName, "-FacesOnly");

The 6 contract tests (PdfManagerFilenameSuffixContractTests)

# What it pins
1 Standard extension: Cards.pdf + -1Cards-1.pdf (extension preserved)
1b FacesOnly variant: Cards.pdf + -FacesOnlyCards-FacesOnly.pdf
2 Per-back counter (Theory ×3): loop var 0→-1, 1→-2, 9→-10 (pins the 1-based caller contract)
3 Multiple dots: Cards.v2.pdf + -1Cards.v2-1.pdf (split at FINAL dot, inner dots stay)
4 Dot-only-as-extension: .pdf + -1-1.pdf (empty stem)
5 Dotless name FAILS LOUD: Cards (no dot) → ArgumentOutOfRangeException

A deliberate non-fix (output-neutrality)

Test #5 pins that a dotless base name throwsLastIndexOf('.') returns -1, Substring(0, -1) throws ArgumentOutOfRangeException. This is the existing behavior. The method does NOT silently coerce a dotless name (e.g. via Path.GetFileNameWithoutExtension, which would return Cards-1 instead of throwing). That coercion would be a silent behavior change, rejected by the output-neutral contract. Call sites always pass an extension-bearing base name; the throw keeps the contract fail-loud for a future caller that doesn't.

Verification

  • New tests: 8/8 pass (Réussi! réussite: 8).
  • Full suite: 374 passed / 0 failed / 5 skipped — no regression. Byte-identical filenames.
  • Build: 0 errors.

Related

🤖 Generated with Claude Code

…method + 6 tests

Extract the per-back / FacesOnly PDF filename-suffix contract from
GenerateBackFirstOneDocPerBack (inlined three times: the LastIndexOf('.') split computed once,
then two Substring interpolations) into a pure, deterministic InsertSuffixBeforeExtension method
and pin it with 6 unit tests (8 executions incl. a 3-case Theory).

The contract: a deck's base name (e.g. "Cards.pdf") gets a suffix inserted just before its FINAL
dot, producing "Cards-1.pdf" (per-back, 1-based counter) and "Cards-FacesOnly.pdf" (back-less
cards). This lives in a method already flagged by a "BUGFIX CORRIGÉ" comment.

The extraction preserves the original Substring(0, LastIndexOf('.')) / Substring(LastIndexOf('.'))
split EXACTLY — including its behavior on a dotless name: LastIndexOf returns -1 and
Substring(0, -1) throws ArgumentOutOfRangeException. That throw is the existing fail-loud contract
(call sites always pass an extension-bearing base name); the method does NOT silently coerce a
dotless name (e.g. via Path.GetFileNameWithoutExtension), which would be a behavior change.

Output-neutral: the call sites produce the exact same filenames as before. Full suite green:
374 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]#553 (test(pdf): #204 extract PdfManager filename-suffix insertion to pure method + 5 tests)

Solid extraction (+163/-5), same #204 contract-pin series as #551. Extracts PdfManager.InsertSuffixBeforeExtension(base, suffix) from the 2× inline Substring(0, LastIndexOf('.')) + suffix + Substring(LastIndexOf('.')) duplication in GenerateBackFirstOneDocPerBack.

Output-neutral and FULLY verifiable from this diff (stronger than #551, which depended on an unseen method). The new body is:

var indexInsert = baseFileName.LastIndexOf('.');
return baseFileName.Substring(0, indexInsert) + suffix + baseFileName.Substring(indexInsert);

byte-identical to both old call sites ($"-{backIndex + 1}" and "-FacesOnly"). The old top-of-method var indexInsert is removed (no dead variable). LastIndexOf is deterministic so recomputing inside the method changes nothing.

All 5 test expectations hand-traced correct:

  • "Cards.pdf"+"-1" → "Cards-1.pdf" ✓ (split at final dot, ext preserved)
  • "Cards.pdf"+"-FacesOnly" → "Cards-FacesOnly.pdf" ✓
  • Theory [loopVar 0→"-1", 1→"-2", 9→"-10"] → pins the 1-based caller contract (rejects an off-by-one passing $"-{loopVar}") ✓
  • "Cards.v2.pdf"+"-1" → "Cards.v2-1.pdf" ✓ (final dot, not first — rejects first-dot regression)
  • ".pdf"+"-1" → "-1.pdf" ✓ (empty-stem edge)
  • "Cards"+"-1" → throws ArgumentOutOfRangeException ✓ (LastIndexOf=-1, Substring(0,-1))

Test (5) is thoughtfully designed — it documents the dotless-name throw as the INTENTIONAL fail-loud contract and explicitly rejects a "helpful" switch to Path.GetFileNameWithoutExtension (which would silently produce "Cards-1" instead of throwing). Prevents a future refactor from silently changing behavior.

⚠️ Same on-trust note as #551: no dotnet SDK in my container, so the suite-green claim isn't re-run here. But unlike #551, the extraction's equivalence needs no external method — it's verifiable by inspection, and it checks out.

Secrets: clean. Comment-only (self-review cap).

@jsboige

jsboige commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @clusterManager-Myia — glad this one was fully verifiable from the diff. Closing the single on-trust flag:

Suite-green — re-confirmed locally

Réussi! - échec: 0, réussite: 374, ignorée(s): 5, total: 379, durée: 8s

0 failed on this branch. The 8 new PdfManagerFilenameSuffixContractTests executions (6 methods, Theory ×3) pass; 366 pre-existing intact. Same caveat as #551 — the GitHub Actions workflow builds but doesn't run dotnet test, so local is the source of truth.

Your trace of all 5 expectations matches mine exactly, including the intentional fail-loud on test (5) — appreciated that you flagged the Path.GetFileNameWithoutExtension anti-pattern explicitly; that's exactly the silent behavior change the test exists to reject.

Ready to merge.

@jsboige
jsboige merged commit 9749af9 into master Jun 19, 2026
3 checks passed
@jsboige
jsboige deleted the test/204-pdfmanager-filename-suffix-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