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

What's in this MR

Stub (internal/storage/stub/)

  • Composite-keyed maps mirror the blob_storage_blobs and upload_sessions UNIQUE 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.
  • DeleteBlob deletes 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) return errors.ErrUnsupported; replaced by Steps 3-4.
  • Every method re-checks ctx.Err() after acquiring s.mu. sync.Mutex is a Go-runtime artifact with no analog in the real impl (PostgreSQL MVCC + unique constraint).
  • WithClock and WithSessionTTL options support deterministic testing.

Conformance suite (internal/storage/testsuites/)

  • Factory func(t *testing.T) storage.BlobStore lets Step 7's real impl register testcontainers cleanup via t.Cleanup.
  • RunSimpleOps with 16 case helpers covers BlobInfo, StoreBlob, OpenBlob, DeleteBlob, and cross-namespace isolation. RunChunkedUpload/RunSessionLifecycle land in Steps 3-4.

Interface-package changes (internal/storage/)

  • BlobOpenOpts/BlobCommitOpts accumulator types exported so out-of-package impls can fold options. Call-site signatures unchanged.
  • BlobMetadata.UserMetadata BlobUserMetadata field added; implementations return a deep copy.

Spec amendment (docs/specs/S06-storage-layer.md)

  • New metadata_sha1 BYTEA NULL column on blob_storage_blobs with CHECK (octet_length = 20). BlobInfo is contractually a single DB round-trip, so UserMetadata must be mirrored.
  • BlobInfo docstring, Object Metadata section, new Acceptance Criterion, and new Resolutions entry all updated. Normalized-column shape chosen over JSONB per docs/dev/database.md.

Tests

  • stub_test.go runs 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.0 reports 0 issues
  • /review-branch ran 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 BlobInfo docstring re: which patterns carry to the real impl.

🤖 Generated with Claude Code

Edited by Pawel Rozlach

Merge request reports

Loading
Loading