← 1.0.0 backlogs ISS-4

The ErrFormat refusal path has no test at all

Type
Bug — missing measurement on a shipped refusal
Found while
SAPE-6
Triage
Fix in 1.0.0
Status
Open
Blocks
SAPE-6
Severity
High — it is the only thing standing between a future upgrade and a misread file

Description

A database file carries a layout version, const Format uint16 = 1 (internal/pager/pager.go:63), separate from the eight-byte Magic. When a build meets a file whose Format it does not read, it must refuse politely and say what to do. That refusal is written and shipped. It has never been run.

Evidence

The refusal exists, in both readers, carrying both numbers:

// internal/pager/led.go:239   (the encrypted reader)
return Meta{}, fmt.Errorf("%w: file says %d, this build reads %d", ErrFormat, format, Format)

// internal/pager/pager.go:573 (the plain reader)
return Meta{}, fmt.Errorf("%w: file says %d, this build reads %d", ErrFormat, format, Format)

The measurement does not:

Triage

Does it lose data, break a promise on a public surface, or stop somebody installing and running? Yes — the first one, on the path that matters most.

This is the refusal that stands between a future build and a file it cannot read. If it does not fire, a build reads a file laid out under different rules as if it were its own: not "not found", not an error, just a database answering questions wrongly. That is the data-loss shape, and it is the exact shape checkOldExtension exists to prevent for the other half of the same problem — and that one has tests.

It is also load-bearing for SAPE-6, which now lists this issue as its blocker. The upgrade rehearsal is "one build writes, the next reads it all back", and the negative half of that rehearsal — the next build declining a file it should decline — is exactly this path. SAPE-6 cannot be called done on top of an untested refusal, so this is not extra scope; it is SAPE-6's floor.

And the work is small, which is why it is in rather than deferred: the refusal is built, both numbers are already in the sentence, and what is missing is a case that writes a header with a Format of 2 and asserts errors.Is(err, ErrFormat) plus the two numbers in the message — for the plain reader and the encrypted one, since they are two copies of the same sentence.

If deferred, what it costs to wait