feat(storage): BlobStore stub + conformance suite (S06 Step 2)
Summary
Implements S06 Step 2: an in-memory storage.BlobStore stub that unblocks Stage 1 of the S06 plan, plus the parameterized conformance suite that re-runs unchanged against the real PostgreSQL-backed implementation in Steps 7-9. Format teams (S10 Maven, S11 npm, S12 Container/OCI) can integrate against the storage contract immediately.
Also amends the S06 spec to surface BlobUserMetadata via BlobInfo, with the single-DB-round-trip rationale (→ DB mirror column) captured in the schema, the Object Metadata section, and the Resolutions list.
References
- Spec: docs/specs/S06-storage-layer.md
- Plan: docs/plans/2026-05-15-storage-layer.md, Step 2
What's in this MR
Stub (internal/storage/stub/)
- Composite-keyed maps mirror the
blob_storage_blobsandupload_sessionsUNIQUE constraints. DB and object-storage halves are kept structurally separate for failure-injection tests. - Four simple-ops methods:
BlobInfo,OpenBlob(stream-only),StoreBlob,DeleteBlob. First-write-wins on content and metadata for cross-format dedup. DeleteBlobdeletes the DB row before the storage object: an interruption leaves orphaned storage (cleaned by ADR-011 reconciliation), never a dangling DB row. Docstring flags an ADR-008 step 6 inconsistency; the stub follows the rationale, not the literal text.- Session methods (
NewSession,ResumeSession,GetSessionStatus) returnerrors.ErrUnsupported; replaced by Steps 3-4. - Every method re-checks
ctx.Err()after acquirings.mu.sync.Mutexis a Go-runtime artifact with no analog in the real impl (PostgreSQL MVCC + unique constraint). WithClockandWithSessionTTLoptions support deterministic testing.
Conformance suite (internal/storage/testsuites/)
Factory func(t *testing.T) storage.BlobStorelets Step 7's real impl registertestcontainerscleanup viat.Cleanup.RunSimpleOpswith 16 case helpers coversBlobInfo,StoreBlob,OpenBlob,DeleteBlob, and cross-namespace isolation.RunChunkedUpload/RunSessionLifecycleland in Steps 3-4.
Interface-package changes (internal/storage/)
BlobOpenOpts/BlobCommitOptsaccumulator types exported so out-of-package impls can fold options. Call-site signatures unchanged.BlobMetadata.UserMetadata BlobUserMetadatafield added; implementations return a deep copy.
Spec amendment (docs/specs/S06-storage-layer.md)
- New
metadata_sha1 BYTEA NULLcolumn onblob_storage_blobswithCHECK (octet_length = 20).BlobInfois contractually a single DB round-trip, soUserMetadatamust be mirrored. BlobInfodocstring, Object Metadata section, new Acceptance Criterion, and new Resolutions entry all updated. Normalized-column shape chosen over JSONB perdocs/dev/database.md.
Tests
stub_test.goruns the conformance suite plus context-cancellation,ErrUnsupported, metadata first-write-wins, and defensive-copy assertions. Public API only.
MR size
~1500 LOC, above the 500-LOC ceiling. ~620 LOC are the conformance suite, explicitly anticipated by the plan. Remaining ~880 LOC are the stub, tests, options change, and spec amendment.
Test plan
-
go build ./...passes -
go test -race ./internal/storage/...passes (19 tests) -
golangci-lint v2.11.0reports 0 issues -
/review-branchran twice; both passes APPROVE
Follow-ups (not blocking)
- Handbook MR to align ADR-008 step 6 literal recommendation with its stated rationale.
- Stub deletion tracked in the roadmap (post-format-impl row).
- Tighten
BlobInfodocstring re: which patterns carry to the real impl.