Skip to content

feat(pkg): natsrouter improvements + mongoutil bulk API + new minioutil#157

Merged
mliu33 merged 11 commits intomainfrom
claude/review-history-service-KWjho
May 7, 2026
Merged

feat(pkg): natsrouter improvements + mongoutil bulk API + new minioutil#157
mliu33 merged 11 commits intomainfrom
claude/review-history-service-KWjho

Conversation

@GITMateuszCharczuk
Copy link
Copy Markdown
Collaborator

@GITMateuszCharczuk GITMateuszCharczuk commented May 7, 2026

Summary

Three independent additions bundled in one branch (per request):

  • pkg/natsrouter — admission control, panic safety, graceful shutdown, HandlerTimeout, doc.go, README rewrite.
  • pkg/mongoutil extension — promotes generic Mongo helpers (Collection[T], OffsetPage, QueryOption) from history-service/internal/mongorepo/ into shared pkg/mongoutil. Adds a three-layer bulk-write API:
    • BulkWrite (foundation, SetOrdered(false), partial-failure result preservation)
    • BulkUpsert (typed convenience, $set MERGE semantics, _id-stripping helper)
    • BulkUpsertByID (ergonomic, bson.M{"_id": idFn(item)} filter)
    • InsertMany returns (int64, error) for partial-failure visibility.
  • pkg/minioutil (new) — typed Bucket[T] JSON-blob wrapper around minio-go/v7. CRUD: Put / Get (NoSuchKey → (nil, nil)) / List (channel-cancellation pattern, goleak verified) / Delete (idempotent). NewBucket fail-fasts on missing bucket; Raw() + Name() escape hatches.

Highlights

  • Spec + plan written and reviewed by 4 expert agents (bug, spec-consistency, senior architecture, mongo expert) across 2 rounds before any code landed.
  • Each task implemented via TDD; per-task spec + code-quality reviews (plus mongo/S3 experts where relevant); fixups applied for all Critical/Important findings.
  • Round-2 reviews caught real bugs that would have hit production:
    • BulkUpsert $set would have rejected every existing-doc update because of immutable _id (fixed via bsonSetWithoutID).
    • goleak.VerifyNone(t) with default ignores would have false-positively flagged net/http.persistConn keepalive goroutines (switched to IgnoreCurrent() baseline).
    • Get godoc claim about HTTP round-trip count was inaccurate (Stat issues a separate HEAD; corrected).
  • pkg/testutil.MinIO(t, prefix) mirrors the existing testutil.MongoDB pattern (sync.Once shared container + per-test fnv-hashed bucket).

Spec & plan

  • docs/superpowers/specs/2026-05-06-mongoutil-extension-and-miniout-design.md
  • docs/superpowers/plans/2026-05-06-mongoutil-extension-and-minioutil.md

15 tasks, each implemented + reviewed serially.

Test plan

  • make lint — 0 issues
  • make test — all unit tests pass
  • make test-integration — Mongo + MinIO testcontainers tests pass (NOTE: this was not runnable in the implementation sandbox due to a Docker socket access quirk; CI must verify)
  • history-service integration tests still pass after the mongorepomongoutil migration
  • BulkUpsert against a 100-record batch (the design center workload)
  • MinIO Put/Get round-trip with the documented application/json; charset=utf-8 content-type
  • List goroutine-leak guard via goleak.IgnoreCurrent() confirms no leak on early break

https://claude.ai/code/session_01Ngkd45NdAxeqX4FU4t6w9X


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Typed MinIO JSON storage with Put/Get/List/Delete semantics
    • Router admission control option (max concurrency) and per-handler timeout middleware
    • HTTP client helper with configurable timeout, headers, bearer token, and logging
  • Documentation

    • Expanded router docs covering concurrency, busy replies, panic backstop, and shutdown behavior
  • Refactor

    • Shared Mongo utilities: typed collection, pagination, and bulk-write helpers
  • Tests

    • Extensive unit/integration tests for MinIO, Mongo utilities, and router concurrency behaviors

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

Warning

Rate limit exceeded

@GITMateuszCharczuk has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 32 minutes and 52 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 22ecb88f-a890-430a-acfa-afeac4f64fb7

📥 Commits

Reviewing files that changed from the base of the PR and between 1f905ec and e5603ff.

📒 Files selected for processing (6)
  • docs/superpowers/plans/2026-04-30-natsrouter-improvements.md
  • docs/superpowers/plans/2026-05-06-mongoutil-extension-and-minioutil.md
  • pkg/natsrouter/doc.go
  • pkg/natsrouter/register.go
  • pkg/natsrouter/router.go
  • pkg/restyutil/restyutil_test.go
📝 Walkthrough

Walkthrough

This PR integrates three major feature areas: (1) extraction of generic MongoDB utilities into a shared pkg/mongoutil package with new bulk-write APIs for heavy upsert workloads; (2) introduction of a new pkg/minioutil typed JSON storage wrapper around MinIO with Put/Get/List/Delete operations; and (3) admission-controlled concurrency and deadline-based timeouts in pkg/natsrouter with improved graceful shutdown. History-service is updated to use the new mongoutil pagination types, and supporting infrastructure includes new go.mod dependencies, test helpers, and comprehensive documentation.

Changes

Mongoutil Package Extraction & Bulk-Write APIs

Layer / File(s) Summary
Type Definitions
pkg/mongoutil/bulk.go
BulkResult struct, UpsertModel/DeleteModel helpers, and internal BSON marshaling utilities (fromDriverResult, bsonSetWithoutID) for bulk-write operations.
Collection Wrapper
pkg/mongoutil/collection.go, pkg/mongoutil/pagination.go, pkg/mongoutil/options.go
Generic Collection[T] wrapper with FindOne/FindMany/Aggregate, AggregatePaged $facet pagination, BulkWrite/BulkUpsert/InsertMany methods. Pagination and query-option types moved from mongorepo with package declaration updates.
Bulk & Collection Tests
pkg/mongoutil/bulk_test.go, pkg/mongoutil/bulk_integration_test.go, pkg/mongoutil/collection_integration_test.go, pkg/mongoutil/empty_input_test.go, pkg/mongoutil/options_test.go
Unit tests for model constructors, BSON utilities, and option application. Integration tests for Find/Aggregate/Bulk operations, partial failure detection, merge semantics, and empty-input short-circuiting.
History-Service Migration
history-service/internal/mongorepo/subscription.go, history-service/internal/mongorepo/threadroom.go, history-service/internal/mongorepo/threadroom_test.go, history-service/internal/service/service.go, history-service/internal/service/mocks/mock_repository.go, history-service/internal/service/threads.go, history-service/internal/service/threads_test.go
All references to pagination types switched from internal mongorepo.OffsetPageRequest/OffsetPage to mongoutil equivalents; SubscriptionRepo collection type updated to mongoutil.Collection[model.Subscription].

Minioutil MinIO JSON Bucket Wrapper

Layer / File(s) Summary
Implementation
pkg/minioutil/minio.go
Typed Bucket[T] wrapper with Put (explicit JSON content-type), Get (NoSuchKey → (nil, nil)), List (prefix filtering, maxKeys capping, goroutine-leak prevention via context.WithCancel), Delete (idempotent), Raw/Name accessors. Non-probing Connect constructor.
Unit & Integration Tests
pkg/minioutil/minio_test.go, pkg/minioutil/minio_integration_test.go
Unit test for Raw/Name accessors. Integration tests for constructor fail-fast bucket existence, Put content-type/overwrite, Get round-trip/missing-key, List prefix/maxKeys with goleak verification, Delete idempotency, malformed-JSON error propagation.

Natsrouter Admission Control & Timeouts

Layer / File(s) Summary
Error Codes
pkg/natsrouter/errors.go, pkg/natsrouter/errors_test.go
New CodeUnavailable constant and ErrUnavailable constructor for saturation replies.
Router Concurrency
pkg/natsrouter/router.go, pkg/natsrouter/router_test.go
WithMaxConcurrency(n) option for semaphore admission gating; reworked New/Default constructors accepting variadic options. Handler dispatch spawns goroutines with WaitGroup tracking, spawn-site panic backstop, and replyBusy saturation handling (immediate ErrUnavailable or fire-and-forget drop). Shutdown reworked to wait for spawned handlers without Add/Wait races.
Middleware & Context
pkg/natsrouter/middleware.go, pkg/natsrouter/middleware_test.go, pkg/natsrouter/context.go, pkg/natsrouter/context_test.go
HandlerTimeout(d) middleware creating deadline-bound contexts via context.WithTimeout. Context.GetHeader(key) method for reading inbound NATS headers with case-sensitive lookup. Unit tests verify deadline setting and parent context non-cancellation; context tests verify header lookup behavior.
Integration Tests
pkg/natsrouter/integration_test.go
Tests for saturation busy replies with WithMaxConcurrency(1), spawn-site panic backstop behavior with internal error reply, and Shutdown waiting for in-flight handlers under load.
Documentation
pkg/natsrouter/README.md, pkg/natsrouter/doc.go, pkg/natsrouter/example_test.go
Expanded README with Concurrency Model, Important Properties (no per-subject FIFO, RegisterVoid drops under saturation), API Reference for variadic New/Default and WithMaxConcurrency, Context usage guidance, error mapping (DeadlineExceeded → ErrUnavailable), panic safety, shutdown contract, and integration testing instructions. Updated doc.go with comprehensive concurrency/panic/shutdown semantics. Example updated to use Default() + HandlerTimeout.

Supporting Infrastructure & Model Updates

Layer / File(s) Summary
Dependencies & Helpers
go.mod, pkg/testutil/minio.go, pkg/testutil/testimages/testimages.go
Added minio-go/v7, testcontainers MinIO module, and goleak; new pkg/testutil.MinIO test helper and pinned MinIO image constant for integration tests.
Test Setup
history-service/internal/mongorepo/setup_test.go
Integration helper setupMongo() for creating test MongoDB database.
Model Updates
pkg/model/employee.go
Employee struct updated with explicit BSON/JSON tags and added org-related fields.
Search-Service Shutdown
search-service/main.go
Reordered shutdown to call router.Shutdown(ctx) before nc.Drain() to ensure spawned handler goroutines complete before NATS draining.
Planning & Specifications
docs/superpowers/plans/2026-04-30-natsrouter-improvements.md, docs/superpowers/plans/2026-05-06-mongoutil-extension-and-minioutil.md, docs/superpowers/specs/2026-05-06-mongoutil-extension-and-miniout-design.md
Detailed implementation plans and design specifications establishing contracts for mongoutil bulk-write APIs, minioutil MinIO integration, and natsrouter admission control with timeout and graceful shutdown.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • hmchangw/chat#131: touches pkg/natsrouter context semantics (SetContext); related to HandlerTimeout/GetHeader changes.
  • hmchangw/chat#144: touches history-service interfaces and signatures affected by pagination/type migrations.
  • hmchangw/chat#118: earlier natsrouter middleware changes; related to middleware ordering and HandlerTimeout interactions.

Suggested reviewers

  • mliu33

"🐰 I hopped through semaphores and buckets bright,
Bulk writes and timeouts kept my paws light;
MinIO stores JSON, tests chase leaks away,
Handlers spawn and shutdown waits — hip hop hooray!"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/review-history-service-KWjho

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
docs/superpowers/specs/2026-05-06-mongoutil-extension-and-miniout-design.md (3)

191-196: 💤 Low value

Add language identifier to fenced code block.

The code block showing the layering pattern (lines 192-196) is missing a language identifier, which triggers a markdownlint warning. Since this is a conceptual diagram rather than executable code, use the text identifier:

-```
+```text
 BulkUpsertByID(items, idFn)
     → BulkUpsert(items, func(x) any { return bson.M{"_id": idFn(x)} })
         → BulkWrite(buildModels(items, filterFn))

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @docs/superpowers/specs/2026-05-06-mongoutil-extension-and-miniout-design.md
around lines 191 - 196, The fenced code block showing the layering pattern lacks
a language identifier; update the block that contains BulkUpsertByID,
BulkUpsert, BulkWrite, buildModels and filterFn to use the text language tag
(i.e., change the opening totext) so markdownlint stops warning while
keeping the conceptual diagram unchanged.


</details>

---

`122-122`: _⚡ Quick win_

**Nil pointer risk with empty-input `(nil, nil)` contract.**

The documented contract states that `BulkWrite`, `BulkUpsert`, and `BulkUpsertByID` return `(nil, nil)` on empty input. This creates a nil pointer dereference risk if callers access fields like `res.Upserted` without first checking `res != nil`.

While line 526-527 notes this was flagged by reviewers and kept as-is, consider documenting a clear usage pattern in the godoc, such as:

```go
// Safe usage pattern:
res, err := col.BulkUpsert(ctx, items, filterFn)
if err != nil { return err }
if res != nil {
    log.Infof("upserted %d items", res.Upserted)
}
```

Alternatively, return `&BulkResult{}` (zero-valued struct) instead of `nil` to eliminate the nil-check requirement while maintaining the same semantic meaning (zero operations performed).





Also applies to: 209-210

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-05-06-mongoutil-extension-and-miniout-design.md`
at line 122, The current godoc contract for BulkWrite, BulkUpsert, and
BulkUpsertByID returns (nil, nil) on empty input which forces callers to
nil-check res before accessing fields like res.Upserted; instead, change the
empty-input behavior to return a zero-valued &BulkResult{} (not nil) so callers
can safely inspect fields without a nil pointer check, and update the godoc for
BulkWrite/BulkUpsert/BulkUpsertByID to show the safe usage pattern (or note that
callers need not check res for nil when empty input returns a zero BulkResult);
ensure references to BulkResult and examples mentioning res.Upserted are updated
accordingly.
```

</details>

---

`446-452`: _💤 Low value_

**Optional: Minor style improvement for readability.**

The out-of-scope list has multiple consecutive bullets starting with "MinIO". Consider varying the sentence structure for better readability:

```diff
- MinIO presigned URLs (`PresignPut` / `PresignGet`).
- MinIO multipart upload helpers.
- MinIO object metadata / tagging / versioning / SSE.
- MinIO event notifications.
- MinIO custom TLS (mTLS, custom CA) — callers needing it construct `*minio.Client` themselves.
+ MinIO presigned URLs (`PresignPut` / `PresignGet`).
+ Multipart upload helpers.
+ Object metadata / tagging / versioning / SSE.
+ Event notifications.
+ Custom TLS (mTLS, custom CA) — callers needing it construct `*minio.Client` themselves.
```

Since all items in this section relate to MinIO, the repeated prefix is redundant.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-05-06-mongoutil-extension-and-miniout-design.md`
around lines 446 - 452, Rewrite the consecutive bullets that all begin with
"MinIO" to remove the repetitive prefix and improve readability: e.g., change
items mentioning "MinIO presigned URLs (`PresignPut` / `PresignGet`)", "MinIO
multipart upload helpers", "MinIO object metadata / tagging / versioning / SSE",
"MinIO event notifications", "MinIO custom TLS (mTLS, custom CA)", "MinIO region
option", and "MinIO bucket creation" into varied phrasing such as "Presigned
URLs (`PresignPut` / `PresignGet`)", "Multipart upload helpers", "Object
metadata/tagging/versioning and SSE", "Event notifications", "Custom TLS (mTLS,
custom CA) — callers needing it construct `*minio.Client` themselves", "Region
option (note: `minio-go` defaults to `us-east-1`)", and "Bucket creation —
provisioned by ops; `NewBucket` only verifies existence" so the list is concise
and avoids repeated "MinIO" prefixes.
```

</details>

</blockquote></details>
<details>
<summary>pkg/natsrouter/integration_test.go (1)</summary><blockquote>

`280-312`: _💤 Low value_

**LGTM — but consider nudging the second `Register` ahead of the first request for clarity.**

The test relies on the spawn-site backstop releasing the semaphore slot and the WaitGroup decrement so the follow-up `ok.{id}` request succeeds. That's the right thing to verify. One small readability tweak: `Register(r, "ok.{id}", ...)` is currently registered *after* the panic round-trip (lines 302-305), which is fine on `*otelnats.Conn` (the inbox round-trip in `nc.Request` flushes pending SUBs), but it reads as if the subscription might not be live yet. Registering both routes up front would avoid the question without changing test semantics.




<details>
<summary>♻️ Optional reorder</summary>

```diff
 	r := natsrouter.New(nc, "integration-panic-backstop", natsrouter.WithMaxConcurrency(2))

 	natsrouter.Register(r, "boom.{id}",
 		func(c *natsrouter.Context, req echoReq) (*echoResp, error) {
 			panic("intentional handler panic")
 		})
+	natsrouter.Register(r, "ok.{id}",
+		func(c *natsrouter.Context, req echoReq) (*echoResp, error) {
+			return &echoResp{Seq: req.Seq}, nil
+		})

 	// Panicking request must receive a reply (not time out) and the
 	// reply must indicate an error.
 	data, _ := json.Marshal(echoReq{Seq: 1})
 	resp, err := nc.Request(context.Background(), "boom.1", data, 5*time.Second)
 	require.NoError(t, err, "panicking handler should still produce a reply via backstop")

 	var payload map[string]any
 	require.NoError(t, json.Unmarshal(resp.Data, &payload))
 	assert.Equal(t, "internal error", payload["error"], "expected internal error reply from backstop")

 	// Process survived: a follow-up normal request must succeed.
-	natsrouter.Register(r, "ok.{id}",
-		func(c *natsrouter.Context, req echoReq) (*echoResp, error) {
-			return &echoResp{Seq: req.Seq}, nil
-		})
 	data, _ = json.Marshal(echoReq{Seq: 2})
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/natsrouter/integration_test.go` around lines 280 - 312, Move the
follow-up subscription so both routes are registered before sending the first
request: in TestIntegration_SpawnSitePanicBackstop register the "ok.{id}"
handler (the natsrouter.Register call that returns echoResp) prior to performing
the nc.Request to "boom.1"; keep the panic handler registration for "boom.{id}"
as-is and ensure the order change only reorders the Register(r, "ok.{id}", ...)
call (no other logic changes).
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @pkg/minioutil/minio_integration_test.go:

  • Around line 192-206: The test TestIntegration_List_DefaultCap currently seeds
    only 5 objects so it never triggers the fallback cap; update the test to insert
    defaultListCap+1 objects (use the same Put loop but iterate to defaultListCap+1)
    before calling b.List(ctx, "k-", 0) and then assert that the returned keys
    length equals defaultListCap to verify the List fallback cap behavior; reference
    NewBucket, b.Put, b.List and the defaultListCap constant when making the change.

In @pkg/minioutil/minio.go:

  • Around line 194-198: The startup probe in Connect uses client.ListBuckets
    which requires account-wide s3:ListAllMyBuckets and is too broad; instead
    perform a bucket-scoped existence/permission check (the same used by NewBucket)
    against the configured bucket name so least-privilege credentials succeed.
    Replace the ListBuckets probe with a bucket-scoped check such as calling the
    existing BucketExists (or equivalent Head/Stat on the configured bucket) using
    the probeCtx and the configured bucket variable, and return the wrapped error
    only if that bucket-scoped check fails; remove the ListBuckets call so Connect
    no longer demands ListAllMyBuckets permission.

Nitpick comments:
In @docs/superpowers/specs/2026-05-06-mongoutil-extension-and-miniout-design.md:

  • Around line 191-196: The fenced code block showing the layering pattern lacks
    a language identifier; update the block that contains BulkUpsertByID,
    BulkUpsert, BulkWrite, buildModels and filterFn to use the text language tag
    (i.e., change the opening totext) so markdownlint stops warning while
    keeping the conceptual diagram unchanged.
  • Line 122: The current godoc contract for BulkWrite, BulkUpsert, and
    BulkUpsertByID returns (nil, nil) on empty input which forces callers to
    nil-check res before accessing fields like res.Upserted; instead, change the
    empty-input behavior to return a zero-valued &BulkResult{} (not nil) so callers
    can safely inspect fields without a nil pointer check, and update the godoc for
    BulkWrite/BulkUpsert/BulkUpsertByID to show the safe usage pattern (or note that
    callers need not check res for nil when empty input returns a zero BulkResult);
    ensure references to BulkResult and examples mentioning res.Upserted are updated
    accordingly.
  • Around line 446-452: Rewrite the consecutive bullets that all begin with
    "MinIO" to remove the repetitive prefix and improve readability: e.g., change
    items mentioning "MinIO presigned URLs (PresignPut / PresignGet)", "MinIO
    multipart upload helpers", "MinIO object metadata / tagging / versioning / SSE",
    "MinIO event notifications", "MinIO custom TLS (mTLS, custom CA)", "MinIO region
    option", and "MinIO bucket creation" into varied phrasing such as "Presigned
    URLs (PresignPut / PresignGet)", "Multipart upload helpers", "Object
    metadata/tagging/versioning and SSE", "Event notifications", "Custom TLS (mTLS,
    custom CA) — callers needing it construct *minio.Client themselves", "Region
    option (note: minio-go defaults to us-east-1)", and "Bucket creation —
    provisioned by ops; NewBucket only verifies existence" so the list is concise
    and avoids repeated "MinIO" prefixes.

In @pkg/natsrouter/integration_test.go:

  • Around line 280-312: Move the follow-up subscription so both routes are
    registered before sending the first request: in
    TestIntegration_SpawnSitePanicBackstop register the "ok.{id}" handler (the
    natsrouter.Register call that returns echoResp) prior to performing the
    nc.Request to "boom.1"; keep the panic handler registration for "boom.{id}"
    as-is and ensure the order change only reorders the Register(r, "ok.{id}", ...)
    call (no other logic changes).

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `e7be9d74-7494-47fc-a9fc-5754bbd4f149`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 394cacdd4e7e085bce5ca73a46a95dd0646e84f1 and b4ebfbd05c5295b499003e869b23ad0177ec7e6c.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `go.sum` is excluded by `!**/*.sum`

</details>

<details>
<summary>📒 Files selected for processing (42)</summary>

* `docs/superpowers/plans/2026-04-30-natsrouter-improvements.md`
* `docs/superpowers/plans/2026-05-06-mongoutil-extension-and-minioutil.md`
* `docs/superpowers/specs/2026-05-06-mongoutil-extension-and-miniout-design.md`
* `go.mod`
* `history-service/internal/mongorepo/collection.go`
* `history-service/internal/mongorepo/setup_test.go`
* `history-service/internal/mongorepo/subscription.go`
* `history-service/internal/mongorepo/subscription_test.go`
* `history-service/internal/mongorepo/threadroom.go`
* `history-service/internal/mongorepo/threadroom_test.go`
* `history-service/internal/service/mocks/mock_repository.go`
* `history-service/internal/service/service.go`
* `history-service/internal/service/threads.go`
* `history-service/internal/service/threads_test.go`
* `pkg/minioutil/minio.go`
* `pkg/minioutil/minio_integration_test.go`
* `pkg/minioutil/minio_test.go`
* `pkg/mongoutil/bulk.go`
* `pkg/mongoutil/bulk_integration_test.go`
* `pkg/mongoutil/bulk_test.go`
* `pkg/mongoutil/collection.go`
* `pkg/mongoutil/collection_integration_test.go`
* `pkg/mongoutil/empty_input_test.go`
* `pkg/mongoutil/options.go`
* `pkg/mongoutil/options_test.go`
* `pkg/mongoutil/pagination.go`
* `pkg/mongoutil/pagination_test.go`
* `pkg/natsrouter/README.md`
* `pkg/natsrouter/context.go`
* `pkg/natsrouter/context_test.go`
* `pkg/natsrouter/doc.go`
* `pkg/natsrouter/errors.go`
* `pkg/natsrouter/errors_test.go`
* `pkg/natsrouter/example_test.go`
* `pkg/natsrouter/integration_test.go`
* `pkg/natsrouter/middleware.go`
* `pkg/natsrouter/middleware_test.go`
* `pkg/natsrouter/router.go`
* `pkg/natsrouter/router_test.go`
* `pkg/testutil/minio.go`
* `pkg/testutil/testimages/testimages.go`
* `search-service/main.go`

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* history-service/internal/mongorepo/collection.go
* history-service/internal/mongorepo/subscription_test.go

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread pkg/minioutil/minio_integration_test.go Outdated
Comment thread pkg/minioutil/minio.go Outdated
GITMateuszCharczuk pushed a commit that referenced this pull request May 7, 2026
Per CodeRabbit review on PR #157:

(MAJOR) Connect's ListBuckets probe required s3:ListAllMyBuckets
(an account-wide IAM permission). Real production deployments scope
credentials to the one bucket the service uses (s3:ListBucket on that
bucket only), so the probe would force callers to grant broader IAM
than they actually need.

NewBucket already performs a bucket-scoped probe via client.BucketExists
(which only requires s3:ListBucket on that bucket), so removing the
ListBuckets probe is a strict improvement: less IAM required, and the
fail-fast-on-misconfig contract is preserved -- it just lives in
NewBucket where it's bucket-scoped.

(MINOR) Renamed TestIntegration_List_DefaultCap to
TestIntegration_List_ZeroMaxKeysReturnsAll. The original name implied
the test exercised the 1000-key cap engaging, but it only seeded 5
keys. Renamed + added a comment explaining why we don't seed 1001
keys (~30s of CI Put traffic for marginal coverage; the cap-engages
behavior is exercised at small N by TestIntegration_List_MaxKeysCap).

(NIT) Added 'text' language identifier to the layering code block in
the spec to silence markdownlint.
GITMateuszCharczuk pushed a commit that referenced this pull request May 7, 2026
… fixups

Updates the spec to reflect what actually shipped after CodeRabbit review
and code-quality fixups landed on PR #157:

- Connect: dropped ListBuckets probe entirely. Spec now documents the
  construct-only behavior and explains the IAM rationale (ListBuckets
  requires s3:ListAllMyBuckets account-wide; NewBucket's BucketExists
  is bucket-scoped and sufficient). Resolution log entries (Q8 and the
  earlier bullet) updated to point at the new "Post-merge amendments"
  section.

- Public API: Bucket[T] now lists Raw() and Name() in the main API
  section (previously only mentioned in the round-2 resolution log).

- New "Post-merge amendments" section documents five changes that
  landed after the round-2 log was finalized: Connect probe removal,
  List MaxKeys plumbing, goleak.IgnoreCurrent baseline, Get's "two
  round trips" godoc correction, bsonSetWithoutID move + error
  wrapping. Each entry explains the rationale and cites the commit.

No code change.
@GITMateuszCharczuk GITMateuszCharczuk force-pushed the claude/review-history-service-KWjho branch from d528378 to 3126cd4 Compare May 7, 2026 03:33
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
pkg/testutil/minio.go (1)

39-40: ⚡ Quick win

Don’t silently discard Terminate errors in init failure paths.

Both cleanup calls ignore errors without comment, which hides container cleanup failures. Please either join/wrap terminate errors into minioInitErr or add an explicit comment that cleanup is best-effort.

♻️ Suggested adjustment
-		_ = container.Terminate(ctx)
-		minioInitErr = fmt.Errorf("get minio endpoint: %w", err)
+		if termErr := container.Terminate(ctx); termErr != nil {
+			minioInitErr = fmt.Errorf("get minio endpoint: %w (terminate minio: %v)", err, termErr)
+			return
+		}
+		minioInitErr = fmt.Errorf("get minio endpoint: %w", err)
 		return
...
-		_ = container.Terminate(ctx)
-		minioInitErr = fmt.Errorf("connect minio: %w", err)
+		if termErr := container.Terminate(ctx); termErr != nil {
+			minioInitErr = fmt.Errorf("connect minio: %w (terminate minio: %v)", err, termErr)
+			return
+		}
+		minioInitErr = fmt.Errorf("connect minio: %w", err)
 		return

As per coding guidelines: "Never ignore errors silently — comment if intentionally discarded."

Also applies to: 48-49

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/testutil/minio.go` around lines 39 - 40, When init fails we currently
call container.Terminate(ctx) and discard its error; update the failure path so
Terminate's error is not silently dropped: capture the error returned by
container.Terminate(ctx) and either wrap it into the existing minioInitErr (e.g.
combine with fmt.Errorf or errors.Join) or append a short explicit comment
stating the call is best-effort and its error may be ignored; apply the same
change for the other cleanup call that currently ignores its return (the second
container.Terminate usage) so both Terminate errors are handled or explicitly
documented.
pkg/natsrouter/integration_test.go (1)

280-312: ⚡ Quick win

Make the panic-backstop test actually prove cleanup.

With WithMaxConcurrency(2), a leaked semaphore slot still leaves enough capacity for the follow-up "ok" request, and this test never observes Shutdown to prove wg.Done() ran. Using a cap of 1, or asserting Shutdown returns after the panic path, would make the regression match what the comment claims.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/natsrouter/integration_test.go` around lines 280 - 312, The test
TestIntegration_SpawnSitePanicBackstop currently uses natsrouter.New(...,
natsrouter.WithMaxConcurrency(2)) so a leaked semaphore slot can still allow the
follow-up "ok" request to succeed; change the concurrency cap to 1 (use
natsrouter.WithMaxConcurrency(1)) so a leaked slot blocks the second request and
the test actually verifies cleanup, or alternatively add an explicit check that
r.Shutdown(...) completes after the panic path to assert wg.Done() was called;
update the invocation in the test to use WithMaxConcurrency(1) (or call
r.Shutdown and assert it returns) and adjust assertions accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/natsrouter/router.go`:
- Around line 258-285: The closeLoop can break on ctx.Done() while some
subscriptions are still draining, so we must not start r.wg.Wait() unless we
know no more r.wg.Add(1) calls are possible; change the code around the
closeLoop/WaitGroup section to record whether the closeLoop completed normally
(e.g. set a flag when the for loop finishes without hitting the ctx.Done() case)
and only create the wgDone goroutine and call r.wg.Wait() when that flag is
true; if the loop broke due to ctx.Done(), skip the wg.Wait() path and just
append the ctx error (as already done) so we avoid the Add/Wait race on r.wg
(referencing closed, subs, closeLoop and r.wg).

---

Nitpick comments:
In `@pkg/natsrouter/integration_test.go`:
- Around line 280-312: The test TestIntegration_SpawnSitePanicBackstop currently
uses natsrouter.New(..., natsrouter.WithMaxConcurrency(2)) so a leaked semaphore
slot can still allow the follow-up "ok" request to succeed; change the
concurrency cap to 1 (use natsrouter.WithMaxConcurrency(1)) so a leaked slot
blocks the second request and the test actually verifies cleanup, or
alternatively add an explicit check that r.Shutdown(...) completes after the
panic path to assert wg.Done() was called; update the invocation in the test to
use WithMaxConcurrency(1) (or call r.Shutdown and assert it returns) and adjust
assertions accordingly.

In `@pkg/testutil/minio.go`:
- Around line 39-40: When init fails we currently call container.Terminate(ctx)
and discard its error; update the failure path so Terminate's error is not
silently dropped: capture the error returned by container.Terminate(ctx) and
either wrap it into the existing minioInitErr (e.g. combine with fmt.Errorf or
errors.Join) or append a short explicit comment stating the call is best-effort
and its error may be ignored; apply the same change for the other cleanup call
that currently ignores its return (the second container.Terminate usage) so both
Terminate errors are handled or explicitly documented.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dd904f0e-03c7-4b83-8d5b-63ca6567d2ba

📥 Commits

Reviewing files that changed from the base of the PR and between b4ebfbd and 3126cd4.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (42)
  • docs/superpowers/plans/2026-04-30-natsrouter-improvements.md
  • docs/superpowers/plans/2026-05-06-mongoutil-extension-and-minioutil.md
  • docs/superpowers/specs/2026-05-06-mongoutil-extension-and-miniout-design.md
  • go.mod
  • history-service/internal/mongorepo/collection.go
  • history-service/internal/mongorepo/setup_test.go
  • history-service/internal/mongorepo/subscription.go
  • history-service/internal/mongorepo/subscription_test.go
  • history-service/internal/mongorepo/threadroom.go
  • history-service/internal/mongorepo/threadroom_test.go
  • history-service/internal/service/mocks/mock_repository.go
  • history-service/internal/service/service.go
  • history-service/internal/service/threads.go
  • history-service/internal/service/threads_test.go
  • pkg/minioutil/minio.go
  • pkg/minioutil/minio_integration_test.go
  • pkg/minioutil/minio_test.go
  • pkg/mongoutil/bulk.go
  • pkg/mongoutil/bulk_integration_test.go
  • pkg/mongoutil/bulk_test.go
  • pkg/mongoutil/collection.go
  • pkg/mongoutil/collection_integration_test.go
  • pkg/mongoutil/empty_input_test.go
  • pkg/mongoutil/options.go
  • pkg/mongoutil/options_test.go
  • pkg/mongoutil/pagination.go
  • pkg/mongoutil/pagination_test.go
  • pkg/natsrouter/README.md
  • pkg/natsrouter/context.go
  • pkg/natsrouter/context_test.go
  • pkg/natsrouter/doc.go
  • pkg/natsrouter/errors.go
  • pkg/natsrouter/errors_test.go
  • pkg/natsrouter/example_test.go
  • pkg/natsrouter/integration_test.go
  • pkg/natsrouter/middleware.go
  • pkg/natsrouter/middleware_test.go
  • pkg/natsrouter/router.go
  • pkg/natsrouter/router_test.go
  • pkg/testutil/minio.go
  • pkg/testutil/testimages/testimages.go
  • search-service/main.go
💤 Files with no reviewable changes (2)
  • history-service/internal/mongorepo/subscription_test.go
  • history-service/internal/mongorepo/collection.go
✅ Files skipped from review due to trivial changes (10)
  • pkg/mongoutil/pagination.go
  • pkg/mongoutil/options.go
  • pkg/minioutil/minio_test.go
  • pkg/mongoutil/pagination_test.go
  • pkg/natsrouter/errors_test.go
  • go.mod
  • pkg/minioutil/minio_integration_test.go
  • pkg/mongoutil/empty_input_test.go
  • pkg/natsrouter/README.md
  • pkg/mongoutil/bulk_integration_test.go
🚧 Files skipped from review as they are similar to previous changes (19)
  • pkg/testutil/testimages/testimages.go
  • history-service/internal/mongorepo/setup_test.go
  • pkg/mongoutil/options_test.go
  • pkg/natsrouter/context.go
  • pkg/natsrouter/example_test.go
  • search-service/main.go
  • history-service/internal/service/service.go
  • history-service/internal/mongorepo/threadroom_test.go
  • history-service/internal/service/threads_test.go
  • history-service/internal/service/mocks/mock_repository.go
  • pkg/natsrouter/router_test.go
  • pkg/mongoutil/bulk.go
  • pkg/mongoutil/collection_integration_test.go
  • history-service/internal/mongorepo/threadroom.go
  • pkg/minioutil/minio.go
  • history-service/internal/mongorepo/subscription.go
  • pkg/natsrouter/errors.go
  • pkg/natsrouter/middleware.go
  • pkg/mongoutil/collection.go

Comment thread pkg/natsrouter/router.go
GITMateuszCharczuk pushed a commit that referenced this pull request May 7, 2026
… test fidelity; document best-effort cleanup

CodeRabbit review on PR #157 surfaced three items on the squashed
branch:

(MAJOR) pkg/natsrouter/router.go — Shutdown's wg.Wait() phase could
race with Add(1) calls from still-draining subscriptions when the
closeLoop broke early on ctx.Done(). Per sync.WaitGroup docs, Add
concurrent with Wait is undefined when the counter is zero (panic
risk). Fix: track allClosed; only enter the wg.Wait block when every
subscription confirmed close. If ctx expired before all subs closed,
surface the error and let the caller's deadline take precedence --
remaining handler goroutines continue in the background until process
exit. Comment block rewritten to explain the invariant.

(NIT) pkg/natsrouter/integration_test.go — TestIntegration_SpawnSite-
PanicBackstop used WithMaxConcurrency(2). With cap=2, a leaked
semaphore slot would still leave capacity for the follow-up "ok"
request, masking a cleanup regression. Switched to cap=1 so a leaked
slot blocks the second request and the test actually observes slot
release. Added a comment explaining why cap=1 is load-bearing.

(NIT) pkg/testutil/minio.go — replaced two `_ = container.Terminate(ctx)`
calls with explicit "best-effort cleanup" comments per CLAUDE.md
"never ignore errors silently — comment if intentionally discarded".
The discarded errors are intentional (init already failed; Docker
reaps the container on test-process exit either way), now documented.
@GITMateuszCharczuk GITMateuszCharczuk force-pushed the claude/review-history-service-KWjho branch from 7069b84 to c3dce7c Compare May 7, 2026 04:06
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
pkg/mongoutil/bulk.go (1)

22-28: 💤 Low value

Return concrete types instead of mongo.WriteModel interface

The guideline "Accept interfaces, return structs" recommends returning concrete types rather than interfaces. Both UpsertModel and DeleteModel should return *mongo.UpdateOneModel and *mongo.DeleteOneModel respectively. Callers collecting these into []mongo.WriteModel will implicitly convert the concrete types.

Proposed change
-func UpsertModel(filter, update any) mongo.WriteModel {
+func UpsertModel(filter, update any) *mongo.UpdateOneModel {
 	return mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true)
 }

-func DeleteModel(filter any) mongo.WriteModel {
+func DeleteModel(filter any) *mongo.DeleteOneModel {
 	return mongo.NewDeleteOneModel().SetFilter(filter)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/mongoutil/bulk.go` around lines 22 - 28, Change the function signatures
to return concrete pointer types instead of the interface: have UpsertModel
return *mongo.UpdateOneModel and DeleteModel return *mongo.DeleteOneModel; keep
the body as-is (mongo.NewUpdateOneModel()... and mongo.NewDeleteOneModel()...)
so callers that collect into []mongo.WriteModel will implicitly convert the
concrete pointers to the interface. Update the function declarations for
UpsertModel and DeleteModel to use the concrete return types and adjust any
local usage if any static typing errors appear.
pkg/natsrouter/integration_test.go (1)

303-305: ⚡ Quick win

Use a typed error payload instead of map[string]any

At Line 303, decoding into map[string]any weakens contract checks for NATS JSON payloads in this test path.

Proposed refactor
-	var payload map[string]any
-	require.NoError(t, json.Unmarshal(resp.Data, &payload))
-	assert.Equal(t, "internal error", payload["error"], "expected internal error reply from backstop")
+	type panicErrResp struct {
+		Error string `json:"error"`
+	}
+	var payload panicErrResp
+	require.NoError(t, json.Unmarshal(resp.Data, &payload))
+	assert.Equal(t, "internal error", payload.Error, "expected internal error reply from backstop")

As per coding guidelines: “All NATS payloads must be JSON — use encoding/json with typed structs … never map[string]interface{}”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/natsrouter/integration_test.go` around lines 303 - 305, Replace the
untyped map decode with a concrete struct for the error payload: define a local
type (e.g., ErrorPayload with field Error string `json:"error"`), unmarshal
resp.Data into that struct (replacing the variable payload map[string]any and
the json.Unmarshal call), keep the require.NoError assertion, and then assert on
the ErrorPayload.Error value (instead of payload["error"]) to enforce the JSON
contract for NATS replies in the test where resp.Data is validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/natsrouter/integration_test.go`:
- Around line 350-356: The test spawns inflight goroutines that call nc.Request
but currently ignores returned errors and never waits for completion; update the
block around inflight, the anonymous goroutine, and the nc.Request call to
collect each request outcome and join the goroutines (e.g., use a sync.WaitGroup
and an error channel or a results slice) so any error from nc.Request (or
marshal) is recorded and asserted in the test instead of being discarded; ensure
you close/collect the channel and fail the test if any request returned an error
so no tail goroutines remain past the assertion boundary.

---

Nitpick comments:
In `@pkg/mongoutil/bulk.go`:
- Around line 22-28: Change the function signatures to return concrete pointer
types instead of the interface: have UpsertModel return *mongo.UpdateOneModel
and DeleteModel return *mongo.DeleteOneModel; keep the body as-is
(mongo.NewUpdateOneModel()... and mongo.NewDeleteOneModel()...) so callers that
collect into []mongo.WriteModel will implicitly convert the concrete pointers to
the interface. Update the function declarations for UpsertModel and DeleteModel
to use the concrete return types and adjust any local usage if any static typing
errors appear.

In `@pkg/natsrouter/integration_test.go`:
- Around line 303-305: Replace the untyped map decode with a concrete struct for
the error payload: define a local type (e.g., ErrorPayload with field Error
string `json:"error"`), unmarshal resp.Data into that struct (replacing the
variable payload map[string]any and the json.Unmarshal call), keep the
require.NoError assertion, and then assert on the ErrorPayload.Error value
(instead of payload["error"]) to enforce the JSON contract for NATS replies in
the test where resp.Data is validated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7c7bc55e-d3ca-4f1c-bb99-f5eb48f65b28

📥 Commits

Reviewing files that changed from the base of the PR and between 3126cd4 and c2da97a.

📒 Files selected for processing (8)
  • pkg/minioutil/minio.go
  • pkg/model/employee.go
  • pkg/mongoutil/bulk.go
  • pkg/mongoutil/collection.go
  • pkg/mongoutil/options.go
  • pkg/natsrouter/integration_test.go
  • pkg/natsrouter/router.go
  • pkg/testutil/minio.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/mongoutil/options.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • pkg/testutil/minio.go
  • pkg/minioutil/minio.go
  • pkg/mongoutil/collection.go
  • pkg/natsrouter/router.go

Comment thread pkg/natsrouter/integration_test.go
claude added 10 commits May 7, 2026 10:00
…n + HandlerTimeout

Five improvements to pkg/natsrouter, designed and reviewed via
docs/superpowers/plans/2026-04-30-natsrouter-improvements.md:

- WithMaxConcurrency option + ErrUnavailable error: bounds in-flight
  handler goroutines with a configurable cap (default unbounded /
  gin-style; set to a positive int to throttle). Excess requests get a
  fast ErrUnavailable reply rather than blocking the consumer.

- Spawn-site panic backstop: a recover() wrapper around the spawned
  handler goroutine releases the semaphore slot and decrements the
  in-flight WaitGroup so a panicking handler can't deadlock the router
  or leak permits. The backstop also publishes a "internal error" reply
  so the requester sees a response rather than a timeout.

- Shutdown waits for in-flight handlers: r.Shutdown(ctx) drains
  the in-flight WaitGroup before returning, with the supplied ctx
  bounding the wait. Prevents the long-standing race where
  nc.Drain() returns before handler goroutines have finished writing
  their replies.

- HandlerTimeout middleware: per-handler deadline that returns
  ErrTimeout when the inner handler runs past its budget. Composes
  cleanly with the spawn-site backstop.

- Default() constructor + doc.go example + README rewrite: newcomers
  can now read pkg/natsrouter/README.md end-to-end and build a
  working service without bouncing through the older spec docs.

Reviewed across multiple rounds by bug, architecture, and NATS-expert
agents; consolidated errata applied. Integration tests exercise the
admission cap, the panic backstop (process survives + follow-up
requests succeed), and the shutdown wait.
Two small fixes to search-service/main.go surfaced during the
natsrouter shutdown work:

- Shutdown now waits for the natsrouter's in-flight handlers before
  draining nc, closing the previous race where requests arriving
  during shutdown could be dropped after the consumer drained but
  before handler goroutines finished.

- Rename Header() -> GetHeader() to match the *otelnats.Conn idiom
  the rest of the codebase uses.
… API

Two changes that share the same package boundary:

1. PROMOTION (refactor). Move the generic Mongo helpers out of
   history-service/internal/mongorepo/ into the shared pkg/mongoutil:
     - Collection[T] generic wrapper (FindOne / FindByID / FindMany
       with (nil, nil) not-found semantics; Aggregate; AggregatePaged
       with $facet pagination + 16 MB caveat documented; Raw escape
       hatch).
     - OffsetPageRequest / OffsetPage[T] / EmptyPage[T] /
       NewOffsetPageRequest pagination types.
     - QueryOption + WithProjection / WithSort / WithLimit / WithSkip
       functional options.
   history-service/internal/mongorepo retains domain-specific repos
   (subscription, threadroom, pipelines) and flips imports to
   mongoutil. The TestCollection_* integration tests extract from
   subscription_test.go into pkg/mongoutil/collection_integration_test.go;
   subscription_test.go keeps only TestSubscriptionRepo_*. New
   mongorepo/setup_test.go is a thin testutil.MongoDB wrapper used by
   the remaining domain tests. Mock file regenerated to use the new
   imports. No logic changes to the domain code -- pure relocation
   plus reference flips.

2. BULK-WRITE API (new). Three layers mirroring FindOne/FindByID:
     - BulkWrite (foundation): wraps mongo-driver's BulkWrite with
       explicit SetOrdered(false), short-circuits on empty input to
       (nil, nil), preserves the partial-success *BulkResult on
       partial failure so callers can inspect WriteErrors via
       errors.As(err, &mongo.BulkWriteException{}).
     - BulkUpsert (typed convenience): builds N UpsertModels from
       items + filter mapper. \$set MERGE semantics (preserves stored
       fields not in T). bsonSetWithoutID strips _id from the marshal
       payload because MongoDB rejects updates to the immutable _id;
       _id is set on insert from the upsert filter.
     - BulkUpsertByID (ergonomic): pure pass-through with built-in
       bson.M{"_id": idFn(item)} filter. Cheapest possible bulk-upsert
       pattern (always-indexed _id, single B-tree lookup).
     - InsertMany (sibling to bulk-upsert): write-only batch with
       SetOrdered(false), returns (int64, error) so partial-failure
       callers see how many items got through.
   BulkResult mirrors mongo.BulkWriteResult fields the wrapper
   exposes; UpsertModel + DeleteModel are stateless write-model
   constructors; fromDriverResult is the driver-to-wrapper mapper.
   Empty-input contract is (nil, nil) for the bulk methods and
   (0, nil) for InsertMany; both are documented at the type level.

Designed via docs/superpowers/specs/2026-05-06-mongoutil-extension-
and-miniout-design.md (spec) and executed via the matching plan.
Spec was reviewed across 2 rounds by bug, spec-consistency, senior
architecture, and Mongo-expert agents before any code landed; each
plan task was implemented via TDD and reviewed by spec-compliance
plus code-quality agents (with Mongo expert on tasks involving
substantive driver interaction). The "Post-merge amendments"
section in the spec captures changes that landed after the
resolution log was finalized (BulkUpsert _id strip, bsonSetWithoutID
location + error wrapping, BulkUpsertByID always-indexed note).
New pkg/minioutil follows the pkg/<provider>util convention used by
cassutil/mongoutil/natsutil/valkeyutil. Surface:

- Connect(ctx, endpoint, useSSL, accessKey, secretKey)(*minio.Client, error)
  Construct-only. Does NOT issue a connectivity probe -- a probe via
  ListBuckets requires s3:ListAllMyBuckets (account-wide IAM); real
  production deployments scope credentials to one bucket via
  s3:ListBucket on that bucket's ARN, so probing here would force
  broader IAM than the package needs. NewBucket carries the actual
  fail-fast probe (bucket-scoped BucketExists). The ctx parameter
  is retained for signature symmetry with valkeyutil.Connect.

- Bucket[T any] + NewBucket[T](ctx, client, bucketName) (*Bucket[T], error)
  Typed wrapper binding a *minio.Client to a single bucket and a
  JSON-marshalable payload type T. NewBucket calls
  client.BucketExists at construction so a misconfigured
  MINIO_BUCKET env var fails the service at startup rather than
  failing every Get/Put silently. Does NOT create the bucket --
  provisioning is owned by ops/IaC.

- Bucket[T].Put(ctx, key, v)
  Marshals v as JSON, uploads with explicit Content-Type:
  application/json; charset=utf-8 so downstream tools (S3 CLI,
  browsers, other languages) can identify the payload format
  without out-of-band knowledge.

- Bucket[T].Get(ctx, key) (*T, error)
  Returns (nil, nil) on missing key, matching Collection.FindOne's
  not-found semantics. The NoSuchKey response surfaces from
  obj.Stat() (minio-go's GetObject is lazy); errors.As against
  minio.ErrorResponse with Code=="NoSuchKey" is the discriminator.
  Two HTTP round trips per Get (HEAD via Stat, then GET via
  Decode); acceptable for the small-JSON-blob workload.

- Bucket[T].List(ctx, prefix, maxKeys) ([]string, error)
  Lists object keys within the bucket (NOT bucket names). maxKeys=0
  falls back to defaultListCap (1000 -- matches S3's per-page cap)
  to prevent accidental unbounded scans on misuse. context.WithCancel
  + defer cancel() is load-bearing: minio-go's ListObjects spawns a
  goroutine that fills the returned channel, and breaking out of
  the range loop without cancelling leaks that goroutine. MaxKeys
  is plumbed through to ListObjectsOptions so the server returns
  only min(maxKeys, 1000) per page rather than the full default.

- Bucket[T].Delete(ctx, key) error
  Idempotent on non-versioned buckets -- relies on S3 native
  semantics (DELETE returns 204 regardless of prior existence).

- Bucket[T].Raw() *minio.Client and Bucket[T].Name() string
  Escape hatches mirroring Collection[T].Raw() so callers needing
  features the wrapper does not surface (presigned URLs, multipart,
  conditional Put, tagging, versioning) can reach the underlying
  client without giving up the bucket binding.

Test infrastructure:

- pkg/testutil.MinIO(t, prefix) mirrors testutil.MongoDB: sync.Once-
  shared MinIO container across the test process plus a per-test
  bucket name derived from a stable fnv hash of t.Name(). Bucket
  names are S3-valid (lowercase letters, digits, hyphens; capped at
  63 chars). Best-effort cleanup with a 30s timeout.

- pkg/testutil/testimages adds the MinIO image pin so all
  minio-touching integration tests track the same version.

- pkg/minioutil/minio_test.go has only the trivial Bucket[T].Raw /
  Name accessor unit test. Hand-rolled S3 stub-server unit tests
  were considered and rejected during round-2 review -- emulating
  S3's wire format is fragile against minio-go upgrades and
  provides false confidence; testcontainers MinIO is fast enough
  (~1m for the full suite) to be the sole behavioral backstop.

- The List integration test uses goleak.IgnoreCurrent() snapshotted
  before the List call to verify the channel-cleanup pattern
  without false-positives from minio-go's HTTP keepalive
  goroutines (IdleConnTimeout=60s, longer than goleak's drain
  budget; goleak's defaults do not cover net/http.persistConn).

New deps (all Apache-2.0):
- github.com/minio/minio-go/v7 -- MinIO Go SDK
- github.com/testcontainers/testcontainers-go/modules/minio -- test only
- go.uber.org/goleak -- test only, for the List leak guard

Designed via docs/superpowers/specs/2026-05-06-mongoutil-extension-
and-miniout-design.md (the same design doc that covers the
mongoutil extension; see prior commit). 7 implementation tasks
(Tasks 9-15), each TDD + reviewed by spec-compliance, code-quality,
and S3-expert agents on the load-bearing tasks (Connect, NewBucket,
Get NoSuchKey, List goroutine cleanup).
… test fidelity; document best-effort cleanup

CodeRabbit review on PR #157 surfaced three items on the squashed
branch:

(MAJOR) pkg/natsrouter/router.go — Shutdown's wg.Wait() phase could
race with Add(1) calls from still-draining subscriptions when the
closeLoop broke early on ctx.Done(). Per sync.WaitGroup docs, Add
concurrent with Wait is undefined when the counter is zero (panic
risk). Fix: track allClosed; only enter the wg.Wait block when every
subscription confirmed close. If ctx expired before all subs closed,
surface the error and let the caller's deadline take precedence --
remaining handler goroutines continue in the background until process
exit. Comment block rewritten to explain the invariant.

(NIT) pkg/natsrouter/integration_test.go — TestIntegration_SpawnSite-
PanicBackstop used WithMaxConcurrency(2). With cap=2, a leaked
semaphore slot would still leave capacity for the follow-up "ok"
request, masking a cleanup regression. Switched to cap=1 so a leaked
slot blocks the second request and the test actually observes slot
release. Added a comment explaining why cap=1 is load-bearing.

(NIT) pkg/testutil/minio.go — replaced two `_ = container.Terminate(ctx)`
calls with explicit "best-effort cleanup" comments per CLAUDE.md
"never ignore errors silently — comment if intentionally discarded".
The discarded errors are intentional (init already failed; Docker
reaps the container on test-process exit either way), now documented.
…Type

Replaces the 3-field Employee placeholder (AccountName, Name, EngName)
with the full schema looked up from the employee MongoDB collection.

New fields cover:
- Identity: ID (_id), EmployeeID, EmployeeCategory, EmployeeRoom,
  Status, RosterCode, SiteID, Company.
- Organisational hierarchy: 8 levels x 4 attrs each (description / id /
  name / tcName) for Department, Department1, Division, Division1,
  Section, Section1, Function, Function1; plus DivTCName, ManagedOrgIDs,
  Orgs []Org, and OrgCode (computed; bson:"-").
- Contact: Mail, Phone, Phones, JOSLocationURL, Location, LocationURL.
- Shift: ShiftCode, ShiftCodeDescEn/Zh, ShiftStartTime, ShiftEndTime.
- Supervisor: SupervisorAccountName, SupervisorID, SupervisorName,
  SupervisorEngName, SupervisorPhone, SupervisorPhones.

Renames the Go field EngName -> EnglishName (bson tag stays "engName"
so wire format is unchanged). Zero callers of model.Employee in the
codebase today, so the rename has no breakage.

Adds two supporting types:
- OrgType (string enum: section1, section, department1, department,
  division1, division, function1, function).
- Org (id / description / name / tcName / type).

Every field has both bson and json tags per CLAUDE.md "All model
structs get both json and bson tags". Empty/optional fields use
omitempty consistently.
Per project rule: max ~2 lines of comment, only when WHY is non-obvious.
Cuts roughly 250 lines of doc that restated MinIO/Mongo SDK behavior,
behavior already implied by function names + signatures, or
multi-paragraph rationales better captured in commit history.

What's kept:
- One-line WHY notes for non-obvious behavior (e.g. (nil, nil)
  not-found contract, FindMany returns []T{} not nil for JSON, $set
  is MERGE not REPLACE, _id stripping is required because Mongo
  rejects updates to immutable _id, defer cancel() is load-bearing
  to avoid a goroutine leak).
- One-line caveats with real footgun risk (omitempty caveat on
  BulkUpsert, 16 MB BSON limit on AggregatePaged $facet).
- A brief explanation of why minioutil.Connect doesn't probe at
  startup (least-privilege IAM rationale).

What's gone:
- Multi-paragraph "Implementation note" sections that restated
  what the code clearly shows.
- Field-by-field BulkResult godoc; replaced with a one-line summary
  + the empty-input contract on the type itself.
- Long pitfall lists where 1 keyword would do.
- "Standard wiring" code-example blocks in godoc.
- Restatements of S3 / Mongo native behavior callers can find in
  upstream docs.

Net: ~250 fewer comment lines across 5 files, no logic change.
CodeRabbit (3 findings):
- pkg/mongoutil/bulk.go: UpsertModel/DeleteModel return concrete
  *mongo.UpdateOneModel/*mongo.DeleteOneModel instead of the
  interface (Go idiom + CLAUDE.md "accept interfaces, return
  structs"). bulk_test.go drops the now-redundant type assertions.
- pkg/natsrouter/integration_test.go panic-backstop test: typed
  payload struct instead of map[string]any (CLAUDE.md "never
  map[string]interface{} for NATS payloads").
- pkg/natsrouter/integration_test.go shutdown-wait test: client
  goroutines now collected via sync.WaitGroup; nc.Request errors
  drain through an error channel and are asserted, no more silent
  drops or tail goroutines past the assertion boundary.

Comment trim (every modified file outside pkg/natsrouter):
Max 2 lines per comment, mostly 1-liners. Cuts the BulkUpsert
godoc from 5 lines to 2; InsertMany 3->1; minioutil.Connect 3->1;
testutil.MinIO 11->2; search-service shutdown 5->1; plus several
2->1 collapses. Zero logic change.
New pkg/restyutil produces a *resty.Client with codebase defaults
wired in: OTel transport (so trace context propagates on every
outbound call), slog response logging at debug level, 30s timeout.
Options compose on top: WithTimeout / WithHeader / WithBearerToken /
WithRetries.

Replaces the raw net/http boilerplate seen in pkg/oidc and
pkg/searchengine — services adopting this can write
client.R().SetContext(ctx).SetBody(req).SetResult(&resp).Post("/path")
instead of building requests, checking statuses, and hand-rolling
JSON decode at every call site.

Adds github.com/go-resty/resty/v2 (Apache-2.0) as the canonical
HTTP client per CLAUDE.md (the rule was previously aspirational --
nothing in the repo used Resty before this).

Tests cover defaults, every Option, end-to-end round-trip via
httptest, and retry-on-5xx behaviour.
…ror + WithTransport, log hygiene)

Three reviewers (bug / code-quality / senior-engineer) consolidated
the same actionable items:

- Drop WithRetries entirely. Resty's default retry condition only
  fires on transport errors, not 5xx, so the option was misleading
  without an accompanying retry-condition helper. YAGNI-applied:
  no current call site needs retries; bring it back when a real
  caller has a concrete retry policy in mind.
- Rename WithRetries' max parameter -> moot (option dropped). The
  shadow of the Go 1.21 builtin `max` is no longer present.
- Add OnError hook so transport errors (connect refused, DNS, ctx
  deadline, TLS) log too. Without it the helper REGRESSED
  observability vs. the raw net/http boilerplate it replaces.
- Add WithTransport(http.RoundTripper) Option. Wraps the supplied
  transport with otelhttp so OTel propagation is preserved by
  construction, even with custom TLS / proxy / dialer. Required
  before pkg/oidc (which needs InsecureSkipVerify in dev) can
  migrate.
- Strip query string from URL in slog log fields. URLs can carry
  ?token=... or ?api_key=... in the wild; CLAUDE.md "Never log
  tokens, passwords, or full message bodies".
- Propagate request_id from ctx into the slog line via
  natsutil.RequestIDFromContext. CLAUDE.md "include in all log
  lines".

Tests cover OnError firing on transport error, WithTransport
preserving the custom round-tripper, request_id propagation, and
query-string stripping.
@GITMateuszCharczuk GITMateuszCharczuk force-pushed the claude/review-history-service-KWjho branch from 10b9828 to 1f905ec Compare May 7, 2026 10:02
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
pkg/restyutil/restyutil_test.go (1)

99-99: ⚡ Quick win

slog.NewTextHandler in tests violates the project's JSON-format logger guideline.

Both logging tests mutate the global default logger using slog.NewTextHandler, and the subsequent Contains assertions are written against text-format output (key=value). The coding guideline explicitly forbids text-format loggers in all **/*.go files — test files included.

Switch to slog.NewJSONHandler and update the assertion patterns to match JSON-encoded fields:

♻️ Proposed fix for `TestLog_PropagatesRequestID` and `TestLog_FiresOnTransportError`
-slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})))
+slog.SetDefault(slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})))

In TestLog_PropagatesRequestID, update assertions accordingly:

-assert.Contains(t, out, "request_id=req-abc-123")
+assert.Contains(t, out, `"request_id":"req-abc-123"`)
-assert.NotContains(t, out, "secret", "query string must be stripped from logs")
+assert.NotContains(t, out, "secret", "query string must be stripped from logs")

In TestLog_FiresOnTransportError:

-assert.Contains(t, buf.String(), "http error")
+assert.Contains(t, buf.String(), `"msg":"http error"`)

As per coding guidelines: "Always use log/slog with JSON format — never use fmt.Println, log.Println, or text-format loggers."

Also applies to: 121-121

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/restyutil/restyutil_test.go` at line 99, Tests currently replace the
global logger with slog.NewTextHandler (via
slog.SetDefault(slog.NewTextHandler(&buf, ...))) which violates the JSON-only
logger guideline; change those setups in TestLog_PropagatesRequestID and
TestLog_FiresOnTransportError to use slog.NewJSONHandler(&buf,
&slog.HandlerOptions{Level: slog.LevelDebug}) and update the assertions that
inspect buf to match JSON-encoded fields (e.g., look for JSON keys/values like
"request_id" or the JSON "level"/"msg" fields instead of key=value text
patterns) so the tests validate JSON output while still using the same buffer
and global logger replacement points.
pkg/natsrouter/router.go (1)

18-29: ⚡ Quick win

Keep Registrar out of the producer package’s public API.

This new exported interface is defined on the implementer side and seems to exist only to support Register* composition inside pkg/natsrouter. That permanently expands the package surface and makes future router changes harder to evolve. Prefer an unexported internal interface or keep accepting *Router until a real external consumer needs an abstraction.

As per coding guidelines: “Define interfaces in the consumer, not the implementer”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/natsrouter/router.go` around lines 18 - 29, Registrar is exported but
only used internally; change it to an unexported interface or remove it and
accept *Router directly to avoid expanding the public API. Specifically, rename
the interface symbol Registrar to registrar (or delete it) and update any
references (addRoute, HandlerFunc usages, and places that type-assert or declare
Registrar) inside pkg/natsrouter to use the unexported registrar or *Router
instead so the interface remains internal and the package surface doesn't
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-04-30-natsrouter-improvements.md`:
- Around line 5-8: Update the plan doc to reflect the implemented behavior:
change the described default semaphore cap from "100" to "unbounded (no default
cap)" and replace mentions of a per-route "WithConcurrency" bulkhead with the
actual implemented router-level "WithMaxConcurrency" option; keep architecture
notes about the single dispatcher goroutine and the semaphore-guarded
spawn-per-message model, retain the `HandlerTimeout(d)` middleware description,
and explicitly mark any superseded design ideas (per-route `WithConcurrency` and
default-100 cap) as deprecated so the doc matches the shipped `pkg/natsrouter`
behavior.

In `@docs/superpowers/plans/2026-05-06-mongoutil-extension-and-minioutil.md`:
- Around line 1791-1806: The Connect function currently performs a ListBuckets
probe that was removed from the spec; remove the probe call and its timeout
context (the probeCtx, cancel, and client.ListBuckets usage) from Connect
(function Connect) so it no longer requires account-wide s3:ListAllMyBuckets,
drop the now-unused "time" import, and update the Connect godoc to state that
the ctx parameter is retained for signature symmetry (and is unused) to match
NewBucket's BucketExists behavior and the post-merge amendments.
- Around line 2569-2587: In TestIntegration_List_MaxKeysCap replace the stale
goleak pattern by updating the teardown call: locate the defer
goleak.VerifyNone(t) at the top of the TestIntegration_List_MaxKeysCap function
and change it to call VerifyNone with the IgnoreCurrent baseline option (use
goleak.IgnoreCurrent()) so the test uses the current-goroutine baseline when
verifying leaks.

In `@pkg/natsrouter/router.go`:
- Around line 176-212: The dispatch path (the goroutine started after
r.admit/release and before r.wg.Add/r.wg.Done) can still start work after
Shutdown returns because Shutdown's timeout path bypasses the wait and does not
prevent late callbacks from calling r.admit/r.wg.Add; add a "router stopping"
gate that Shutdown sets (e.g., an atomic or mutex-protected boolean like
r.stopping) and check it in the dispatch path immediately after admittance and
before r.wg.Add (and also before spawning the goroutine and before calling
acquireContext), rejecting requests via r.replyBusy when stopping is true;
ensure Shutdown sets this flag before changing allClosed and after signaling
drains so no new handlers can increment r.wg, and apply the same guard to the
alternate dispatch block around lines 258-293.

---

Nitpick comments:
In `@pkg/natsrouter/router.go`:
- Around line 18-29: Registrar is exported but only used internally; change it
to an unexported interface or remove it and accept *Router directly to avoid
expanding the public API. Specifically, rename the interface symbol Registrar to
registrar (or delete it) and update any references (addRoute, HandlerFunc
usages, and places that type-assert or declare Registrar) inside pkg/natsrouter
to use the unexported registrar or *Router instead so the interface remains
internal and the package surface doesn't change.

In `@pkg/restyutil/restyutil_test.go`:
- Line 99: Tests currently replace the global logger with slog.NewTextHandler
(via slog.SetDefault(slog.NewTextHandler(&buf, ...))) which violates the
JSON-only logger guideline; change those setups in TestLog_PropagatesRequestID
and TestLog_FiresOnTransportError to use slog.NewJSONHandler(&buf,
&slog.HandlerOptions{Level: slog.LevelDebug}) and update the assertions that
inspect buf to match JSON-encoded fields (e.g., look for JSON keys/values like
"request_id" or the JSON "level"/"msg" fields instead of key=value text
patterns) so the tests validate JSON output while still using the same buffer
and global logger replacement points.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1018e473-71f5-4546-bfaa-2ff8ba101ef6

📥 Commits

Reviewing files that changed from the base of the PR and between c2da97a and 1f905ec.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (44)
  • docs/superpowers/plans/2026-04-30-natsrouter-improvements.md
  • docs/superpowers/plans/2026-05-06-mongoutil-extension-and-minioutil.md
  • docs/superpowers/specs/2026-05-06-mongoutil-extension-and-miniout-design.md
  • go.mod
  • history-service/internal/mongorepo/setup_test.go
  • history-service/internal/mongorepo/subscription.go
  • history-service/internal/mongorepo/subscription_test.go
  • history-service/internal/mongorepo/threadroom.go
  • history-service/internal/mongorepo/threadroom_test.go
  • history-service/internal/service/mocks/mock_repository.go
  • history-service/internal/service/service.go
  • history-service/internal/service/threads.go
  • history-service/internal/service/threads_test.go
  • pkg/minioutil/minio.go
  • pkg/minioutil/minio_integration_test.go
  • pkg/minioutil/minio_test.go
  • pkg/model/employee.go
  • pkg/mongoutil/bulk.go
  • pkg/mongoutil/bulk_integration_test.go
  • pkg/mongoutil/bulk_test.go
  • pkg/mongoutil/collection.go
  • pkg/mongoutil/collection_integration_test.go
  • pkg/mongoutil/empty_input_test.go
  • pkg/mongoutil/options.go
  • pkg/mongoutil/options_test.go
  • pkg/mongoutil/pagination.go
  • pkg/mongoutil/pagination_test.go
  • pkg/natsrouter/README.md
  • pkg/natsrouter/context.go
  • pkg/natsrouter/context_test.go
  • pkg/natsrouter/doc.go
  • pkg/natsrouter/errors.go
  • pkg/natsrouter/errors_test.go
  • pkg/natsrouter/example_test.go
  • pkg/natsrouter/integration_test.go
  • pkg/natsrouter/middleware.go
  • pkg/natsrouter/middleware_test.go
  • pkg/natsrouter/router.go
  • pkg/natsrouter/router_test.go
  • pkg/restyutil/restyutil.go
  • pkg/restyutil/restyutil_test.go
  • pkg/testutil/minio.go
  • pkg/testutil/testimages/testimages.go
  • search-service/main.go
💤 Files with no reviewable changes (1)
  • history-service/internal/mongorepo/subscription_test.go
✅ Files skipped from review due to trivial changes (12)
  • pkg/mongoutil/pagination_test.go
  • pkg/natsrouter/errors_test.go
  • pkg/mongoutil/pagination.go
  • pkg/mongoutil/options.go
  • pkg/natsrouter/middleware_test.go
  • history-service/internal/service/service.go
  • pkg/mongoutil/bulk_test.go
  • pkg/natsrouter/errors.go
  • pkg/restyutil/restyutil.go
  • pkg/mongoutil/empty_input_test.go
  • pkg/minioutil/minio.go
  • go.mod
🚧 Files skipped from review as they are similar to previous changes (18)
  • pkg/minioutil/minio_test.go
  • pkg/testutil/testimages/testimages.go
  • pkg/natsrouter/context.go
  • pkg/natsrouter/context_test.go
  • history-service/internal/mongorepo/threadroom_test.go
  • history-service/internal/mongorepo/setup_test.go
  • search-service/main.go
  • pkg/mongoutil/options_test.go
  • pkg/natsrouter/example_test.go
  • pkg/natsrouter/doc.go
  • pkg/mongoutil/bulk.go
  • pkg/mongoutil/collection_integration_test.go
  • history-service/internal/service/threads_test.go
  • pkg/minioutil/minio_integration_test.go
  • pkg/model/employee.go
  • history-service/internal/service/mocks/mock_repository.go
  • pkg/mongoutil/collection.go
  • pkg/natsrouter/README.md

Comment thread docs/superpowers/plans/2026-04-30-natsrouter-improvements.md Outdated
Comment thread docs/superpowers/plans/2026-05-06-mongoutil-extension-and-minioutil.md Outdated
Comment thread pkg/natsrouter/router.go
…lan docs

CodeRabbit pass on the rebased branch surfaced 1 real bug + 5 cleanups.

(MAJOR) router.go: Shutdown previously had a window where late NATS
callbacks (subscription mid-drain or fired after ctx expiry) could
still call admit() + wg.Add(1) + spawn a handler goroutine AFTER
Shutdown returned. That goroutine could then race teardown of caller-
owned dependencies (DB closed, NATS drained). Add a stopping atomic.Bool
gate that Shutdown sets BEFORE any drain step; natsHandler checks it
first and replies busy instead of admitting. Existing in-flight
handlers still drain through the wg.Wait path; only NEW dispatches
post-Shutdown-start are rejected.

(NIT) router.go: removed the exported Registrar interface — used only
by Register/RegisterNoBody/RegisterVoid in this package, no external
mocks, no alternative implementation. Their first parameter is now
*Router directly. Pure simplification: existing callers (which pass
*Router today) keep working. CLAUDE.md "interfaces in the consumer".

(NIT) restyutil_test.go: switched the two log-capture tests from
slog.NewTextHandler to NewJSONHandler; CLAUDE.md "always JSON, never
text-format". Updated the matched assertions.

(DOC) 2026-04-30-natsrouter-improvements.md: marked the original
"default 100" + per-route WithConcurrency wording as superseded with
a post-implementation note. Shipped behavior: unbounded default,
single router-level WithMaxConcurrency.

(DOC) 2026-05-06-mongoutil-extension-and-minioutil.md Task 9: dropped
the ListBuckets startup probe from the Connect code block; added a
post-implementation note pointing at the spec's amendments section.
Future agents executing this plan get the IAM-compliant code.

(DOC) Same plan, Task 14: replaced the stale defer goleak.VerifyNone(t)
pattern with goleak.IgnoreCurrent() baseline; updated the comment to
explain why minio-go's HTTP keepalive goroutines (IdleConnTimeout=60s)
require it.
Copy link
Copy Markdown
Collaborator

@mliu33 mliu33 left a comment

Choose a reason for hiding this comment

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

Excellent! Thanks!

Comment thread pkg/natsrouter/router.go
// If multiple WithMaxConcurrency options are supplied, the last call
// with a positive n takes effect. Saturation triggers a 503-style
// ErrUnavailable reply.
func WithMaxConcurrency(n int) Option {
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.

Super!

}

func logError(req *resty.Request, err error) {
slog.Debug("http error", logFields(req, 0, time.Since(req.Time), err)...)
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.

maybe it's better to use slog.Error for log error ?

return nil
}

func logError(req *resty.Request, err error) {
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.

maybe it's better to include resp.StatusCode when logging error response ?

Comment thread pkg/natsrouter/router.go
return
}
r.wg.Add(1)
go func() {
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.

excellent! thanks

@mliu33 mliu33 merged commit ee87139 into main May 7, 2026
22 checks passed
GITMateuszCharczuk pushed a commit that referenced this pull request May 8, 2026
…l/Employee

Two changes:

1. logError uses slog.Error instead of slog.Debug, matching the
   codebase convention (mongoutil.Disconnect, natsutil.Reply/Ack,
   natsrouter handler-error all use Error). @mliu33 PR #157 review.

2. Delete pkg/model/employee.go entirely. The struct had zero
   callers in the repo. No replacement; bring back when a real
   user shows up.

logResponse stays at slog.Debug -- status code was already in the
log fields (logFields includes resp.StatusCode), no level change
needed beyond what @mliu33 actually requested.
GITMateuszCharczuk pushed a commit that referenced this pull request May 8, 2026
…l/Employee

Two changes:

1. logError uses slog.Error instead of slog.Debug, matching the
   codebase convention (mongoutil.Disconnect, natsutil.Reply/Ack,
   natsrouter handler-error all use Error). @mliu33 PR #157 review.

2. Delete pkg/model/employee.go entirely. The struct had zero
   callers in the repo. No replacement; bring back when a real
   user shows up.

logResponse stays at slog.Debug -- status code was already in the
log fields (logFields includes resp.StatusCode), no level change
needed beyond what @mliu33 actually requested.
Joey0538 pushed a commit that referenced this pull request May 8, 2026
…l/Employee (#163)

Two changes:

1. logError uses slog.Error instead of slog.Debug, matching the
   codebase convention (mongoutil.Disconnect, natsutil.Reply/Ack,
   natsrouter handler-error all use Error). @mliu33 PR #157 review.

2. Delete pkg/model/employee.go entirely. The struct had zero
   callers in the repo. No replacement; bring back when a real
   user shows up.

logResponse stays at slog.Debug -- status code was already in the
log fields (logFields includes resp.StatusCode), no level change
needed beyond what @mliu33 actually requested.

Co-authored-by: Claude <noreply@anthropic.com>
Joey0538 added a commit that referenced this pull request May 8, 2026
ECK and most managed ES clusters require credentials. Add SEARCH_USERNAME
and SEARCH_PASSWORD env vars on search-service and search-sync-worker
(both default to empty so dev clusters without auth still work).

Also refactor searchengine.New to take a Config struct rather than
growing positional args (now five connection knobs: Backend, URL,
Username, Password, TLSSkipVerify) — labeled fields at every call site
and easy to extend. Test call sites (9) updated to the struct form.

Drive-by: fix history-service/internal/mongorepo/room.go and
service.go to import the Collection / NewCollection / WithProjection
helpers from pkg/mongoutil (they were moved out of the local package
in #157 but room.go and the compile-time check were left referencing
the old package-local names, breaking make lint on main).

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp

Co-authored-by: Claude <noreply@anthropic.com>
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.

3 participants