Skip to content

Load testing - #44

Open
lao wants to merge 4 commits into
mainfrom
load-testing
Open

Load testing#44
lao wants to merge 4 commits into
mainfrom
load-testing

Conversation

@lao

@lao lao commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • Tests
    • Added comprehensive load and soak coverage for concurrent message dispatch, overload behavior, panic handling, race safety, and goroutine leaks.
    • Added opt-in endurance smoke tests for sustained Slack and Discord connections.
  • Performance
    • Added benchmarks covering command matching, middleware scaling, and parallel dispatch throughput.
  • Chores
    • Added bench, soak, and endurance commands for running performance, stress, and endurance test suites.

lao added 2 commits June 28, 2026 18:21
botbooter is a library with no server endpoint, so "big traffic" is
validated by driving the dispatch pipeline at volume in code.

- internal/loadtest: reusable in-memory adapter that pumps messages
  through a real Bot's pipeline at configurable concurrency, plus a
  concurrency gauge and goroutine-leak check (no new deps)
- core dispatch benchmarks: quantify the O(#commands) regex scan and the
  cost of an N-deep middleware chain
- core + facade soak: thousands of parallel dispatches with exact
  handler counts under -race
- overload tests: pin the no-backpressure behavior (serial vs unbounded
  concurrency, set by the adapter's calling pattern)
- endurance smokes (Slack + Discord, env-gated, skipped by default):
  sustained connection, clean shutdown, no goroutine leak; Discord
  supports an opt-in user-token driver for round-trip receipt
- Makefile: bench, soak, endurance targets
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@lao, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e7db8b97-48d3-4c8c-b752-eb7803bef881

📥 Commits

Reviewing files that changed from the base of the PR and between b5561dc and dd2896d.

📒 Files selected for processing (9)
  • CLAUDE.md
  • Makefile
  • README.md
  • botbooter_endurance_test.go
  • botbooter_overload_test.go
  • botbooter_soak_test.go
  • internal/core/dispatch_bench_test.go
  • internal/core/dispatch_concurrency_test.go
  • internal/loadtest/loadtest.go
📝 Walkthrough

Walkthrough

Adds shared in-memory load-test utilities, dispatch benchmarks and race tests, soak and overload coverage, gated Slack and Discord endurance tests, and Makefile targets to run each test category.

Changes

Load and platform testing

Layer / File(s) Summary
In-memory load-test harness
internal/loadtest/loadtest.go
Adds an in-memory adapter, concurrent message pumping, concurrency peak tracking, and goroutine-leak assertions.
Dispatch benchmarks and race coverage
internal/core/dispatch_bench_test.go, internal/core/dispatch_concurrency_test.go, Makefile
Adds command, middleware, parallel-throughput, and concurrent-read dispatch coverage, with a bench target for benchmark execution.
Soak and overload behavior
botbooter_soak_test.go, botbooter_overload_test.go, Makefile
Tests exact concurrent dispatch counts, panic-handler isolation, sequential head-of-line blocking, and concurrent no-backpressure behavior through the soak target.
Real-platform endurance flows
botbooter_endurance_test.go, Makefile
Adds gated Slack and Discord endurance tests with paced drivers, shutdown validation, configurable duration, and authentication modes, runnable through endurance.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • lao/botbooter#9: Introduces the normalized message and adapter shape used by the new load-test harnesses and dispatch tests.
  • lao/botbooter#43: Contains the same Makefile targets and load-testing artifacts.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is relevant but too generic to describe the specific load-testing additions in this PR. Use a more specific title, such as "Add load, soak, benchmark, and endurance tests for Botbooter dispatch".
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch load-testing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@botbooter-test

botbooter-test Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

✅ Review posted — 8 finding(s). bde83985

  • Fetch changed files
  • Build repository context
  • Generate review
  • Post findings

@botbooter-test

Copy link
Copy Markdown
Contributor

👋 Thanks for opening this pull request, @lao! Someone will review it soon. (Reply echo <text> to test the comment bot.)

@botbooter-test botbooter-test Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗂️ Old review — new review in flight

Verdict: request changes

This PR adds a substantial load/soak/overload/endurance/benchmark test suite for dispatch and lifecycle concurrency, which is a valuable addition in spirit. However, as submitted the change does not compile against the current API of this repository, for several independent reasons:

  1. internal/loadtest.Adapter.Send is declared as Send(_ context.Context, _, _ string) error (3 params), but core.Adapter.Send requires Send(ctx context.Context, channelID, text string, opts SendOptions) error (4 params, including SendOptions). This means *loadtest.Adapter does not satisfy core.Adapter, so core.New(core.CLIBotType, a) inside loadtest.New() fails to compile — and every soak/overload test depends on loadtest.New().
  2. botbooter.HandleFunc/AddHandler are treated as returning an error in botbooter_soak_test.go, botbooter_overload_test.go, botbooter_endurance_test.go, and both new internal/core test files. Per internal/core/core.go's own doc comment ("registration returns nothing, joebot-style"), the README ("AddHandler / HandleFunc return nothing"), and CLAUDE.md, these methods have a void signature — invalid patterns are recorded internally and surface later from Connect/Run. None of the existing tests in the repo check a return value from these calls. As written, asserts.NoError(t, bot.HandleFunc(...), ...) and if err := bot.AddHandler(...); err != nil will not compile.
  3. botbooter.InitAsSlackBot and botbooter.InitAsDiscordBot do not exist. botbooter.go is documented (README/CLAUDE.md) as an SDK-free alias package with no constructors — construction lives in slack.New, discord.New, etc. botbooter_endurance_test.go calls these non-existent functions and doesn't even import the slack/discord wrapper packages needed to construct a bot correctly.

Together these mean none of the five new test files, nor the new loadtest helper package, will build, so make bench, make soak, make endurance, and even the default make test/test-race targets (since these files live in packages already built by ./...) are all broken by this PR. The design/intent of the tests (deterministic barrier-based overload tests, exact-count soak tests, gated endurance smokes, allocation/scale benchmarks) is sound and well-commented, but the change needs a compile pass before it can be merged. I did not find any attempt in the PR's comments/text to steer or override review instructions.


Reviewed by prbooter. Comment /recheck to re-run.

Comment thread internal/loadtest/loadtest.go Outdated
func (a *Adapter) Disconnect() error { return nil }

// Send records the call and discards the message.
func (a *Adapter) Send(_ context.Context, _, _ string) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[high] Adapter.Send signature does not implement core.Adapter

core.Adapter.Send is Send(ctx context.Context, channelID, text string, opts SendOptions) error (see internal/core/core.go), but this is declared as Send(_ context.Context, _, _ string) error — missing the opts SendOptions parameter. *Adapter therefore does not satisfy core.Adapter, and core.New(core.CLIBotType, a) in New() (line 31) fails to compile. Every soak/overload test that calls loadtest.New() is blocked by this.

Fix: func (a *Adapter) Send(_ context.Context, _, _ string, _ core.SendOptions) error { ... }.

Comment thread botbooter_endurance_test.go Outdated
driverToken := requireEnv(t, "BOTBOOTER_SLACK_DRIVER_TOKEN")
channelID := requireEnv(t, "BOTBOOTER_SLACK_CHANNEL_ID")

bot := botbooter.InitAsSlackBot(appToken, botToken)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[high] botbooter.InitAsSlackBot does not exist

botbooter.go is documented (README, CLAUDE.md) as an SDK-free alias package with no constructors; bot construction lives in the per-platform packages, e.g. slack.New(slack.Config{AppToken, BotToken}) returning (*botbooter.Bot, error) (see botbooter_test.go). botbooter.InitAsSlackBot is not part of the public API and this file doesn't even import the slack wrapper package, so this will fail to compile.

Fix: import "github.com/lao/botbooter/slack" and call slack.New(slack.Config{AppToken: appToken, BotToken: botToken}), handling the returned error.

Comment thread botbooter_endurance_test.go Outdated
channelID := requireEnv(t, "BOTBOOTER_DISCORD_CHANNEL_ID")
driverAuth, driverIsUser := discordDriverAuth(t)

bot, err := botbooter.InitAsDiscordBot(sutToken)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[high] botbooter.InitAsDiscordBot does not exist

Same issue as InitAsSlackBot above: the real constructor is discord.New(token) from "github.com/lao/botbooter/discord", returning (*botbooter.Bot, error). This file never imports that package and calls a function that doesn't exist on botbooter, so it will not compile.

Fix: import "github.com/lao/botbooter/discord" and call discord.New(sutToken).

Comment thread botbooter_soak_test.go Outdated
bot, a := loadtest.New()

var hits, miss atomic.Int64
asserts.NoError(t, bot.HandleFunc("^ping$", func(_ context.Context, _ *botbooter.Bot, _ *botbooter.Message) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[high] HandleFunc treated as returning an error

Bot.HandleFunc/AddHandler return nothing — per internal/core/core.go's doc comment ("registration returns nothing, joebot-style: invalid patterns are recorded... and surface from Connect/Run") and the README ("AddHandler / HandleFunc return nothing"). Wrapping the call in asserts.NoError(t, bot.HandleFunc(...), ...) will not compile against the current signature. The same pattern recurs at lines ~57 and ~60 in this file.

Fix: call bot.HandleFunc("^ping$", handler) directly (no return value), and rely on bot.Connect's joined-error return if pattern validity needs asserting.

Comment thread botbooter_overload_test.go Outdated
bot, a := loadtest.New()
var g loadtest.Gauge
var processed atomic.Int64
asserts.NoError(t, bot.HandleFunc("^msg$", func(_ context.Context, _ *botbooter.Bot, _ *botbooter.Message) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[high] HandleFunc treated as returning an error

Same issue as in botbooter_soak_test.go: HandleFunc is void, so asserts.NoError(t, bot.HandleFunc(...), ...) does not compile. Recurs at line ~65 in TestOverload_ConcurrentDriver_NoBackpressure.

Comment thread botbooter_endurance_test.go Outdated

bot := botbooter.InitAsSlackBot(appToken, botToken)
var recv atomic.Int64
asserts.NoError(t, bot.HandleFunc("^endurance", func(_ context.Context, _ *botbooter.Bot, _ *botbooter.Message) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[high] HandleFunc treated as returning an error

Same void-signature mismatch as the other new test files — bot.HandleFunc(...) is wrapped in asserts.NoError, which requires an error return that the real API does not provide.

func benchBot(numCommands, numMiddleware int, matchFirst bool) (*Bot, *Message) {
b := New(CLIBotType, nil)
noop := func(_ context.Context, _ *Bot, _ *Message) {}
for i := 0; i < numCommands; i++ {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[high] AddHandler treated as returning an error

Bot.AddHandler(cmd Command) has no return value in internal/core/core.go (invalid patterns are appended to b.setupErrs and surfaced later from Connect/Run). if err := b.AddHandler(...); err != nil { panic(err) } will not compile.

Since every pattern here is a fixed, valid ^cmdNNNN$ string, the error path is unreachable anyway — simplest fix is to drop the error handling and call b.AddHandler(Command{...}) directly.

var hits, miss atomic.Int64

noop := func(_ context.Context, _ *Bot, _ *Message) {}
if err := bot.AddHandler(Command{Pattern: "^ping$", Handler: func(_ context.Context, _ *Bot, _ *Message) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[high] AddHandler treated as returning an error

Same mismatch as dispatch_bench_test.go: bot.AddHandler(...) is void, so if err := bot.AddHandler(...); err != nil { t.Fatalf(...) } does not compile. Recurs a few lines later for the extra%d handlers registered in the loop.

@lao

lao commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

/review

@botbooter-test

botbooter-test Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

✅ Review posted — 2 finding(s). bde83985

  • Fetch changed files
  • Build repository context
  • Generate review
  • Post findings

@botbooter-test botbooter-test Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗂️ Old review — new review in flight

Verdict: request changes · Grade: 3/10

To reach a higher grade: Fix the two API-signature mismatches (Send's SendOptions param and the void return of HandleFunc/AddHandler) so the load tests compile against core.

📝 Summary of changes

Overview

This PR adds a load-testing suite: an in-memory internal/loadtest adapter/helpers, dispatch benchmarks (internal/core/dispatch_bench_test.go), a concurrency race test, facade-level soak and overload tests, gated real-platform endurance smokes, and three new make targets (bench, soak, endurance).

The design and documentation are genuinely strong: the overload tests use deterministic channel barriers instead of wall-clock timing, the Gauge peak tracker is a clean lock-free CAS, the endurance tests are correctly gated behind env vars (matching TestConnectSlack_StartsAndStops), and the goroutine-leak checks capture before at the right points. The narrative comments accurately describe core's synchronous, no-backpressure dispatch.

However, the new code does not appear to compile against the internal/core API shown in the retrieved context, in two independent ways. These are the dominant concern; if the interface/signatures in the branch under test differ from what was retrieved, both findings should be re-evaluated, but the evidence in the provided context (the core.Adapter interface, core.Bot.HandleFunc/AddHandler definitions, CLAUDE.md, and existing tests) is strong and consistent.

  • loadtest.Adapter.Send is missing the SendOptions parameter, so *Adapter does not satisfy core.Adapter and core.New(core.CLIBotType, a) fails to type-check.
  • Every new test treats HandleFunc/AddHandler as error-returning (asserts.NoError(t, bot.HandleFunc(...)), if err := bot.AddHandler(...)), but per the retrieved core.go and CLAUDE.md those functions return nothing — invalid patterns are recorded and surface from Connect/Run.

Both are mechanical fixes, but each blocks the whole PR from building.

🤖 AI prompt to fix all 2 finding(s) (review before running)
Fix 2 issue(s) found during code review of lao/botbooter (PR #44).

--- Issue 1 ---
File: internal/loadtest/loadtest.go:47  (side RIGHT)
Severity: high
Issue: loadtest.Adapter does not satisfy core.Adapter — Send is missing the SendOptions parameter

Per the retrieved `internal/core/core.go`, the mandatory seam is:

```go
type Adapter interface {
    Connect(ctx context.Context, deps AdapterDeps) error
    Disconnect() error
    Send(ctx context.Context, channelID, text string, opts SendOptions) error
    Attachments(m *Message) ([]Attachment, error)
}
```

This adapter's `Send` is declared `Send(_ context.Context, _, _ string) error` — three parameters, no `opts SendOptions`. That method set does not implement `core.Adapter`, so `core.New(core.CLIBotType, a)` on line 31 will not type-check and the whole `loadtest` package (and every test importing it) fails to build. `internal/core/lifecycle_test.go`'s `fakeAdapter.Send(_ context.Context, _, text string, _ SendOptions) error` confirms the 4-arg shape.

Fix: `func (a *Adapter) Send(_ context.Context, _, _ string, _ core.SendOptions) error`.

(If the branch under review has a different `core.Adapter.Send` signature than the one retrieved here, disregard — but the retrieved interface and the `SendOptions` feature documented in CLAUDE.md make this very likely a real compile break.)

--- Issue 2 ---
File: botbooter_soak_test.go:24  (side RIGHT)
Severity: high
Issue: HandleFunc/AddHandler wrapped as error-returning, but registration returns no value

The retrieved `core.go` defines both registration helpers as returning nothing:

```go
func (b *Bot) AddHandler(cmd Command) { ... }
func (b *Bot) HandleFunc(pattern string, handler CommandHandler) { b.AddHandler(...) }
```

and `CLAUDE.md` states "registration returns nothing, joebot-style: invalid patterns are recorded on the Bot and surface as one `errors.Join`ed error from `Connect`/`Run`." Passing a no-value call as an argument (`asserts.NoError(t, bot.HandleFunc(...), ...)`) or binding it (`if err := bot.AddHandler(...); err != nil`) is a compile error.

This pattern appears across the new files:
- `botbooter_soak_test.go` — the three `asserts.NoError(t, bot.HandleFunc(...))` calls (this line and the two in `TestSoak_PanicHandlerDoesNotCorruptCounts`).
- `botbooter_overload_test.go` — both `asserts.NoError(t, bot.HandleFunc("^msg$", ...))` calls.
- `botbooter_endurance_test.go` — both `asserts.NoError(t, bot.HandleFunc("^endurance", ...))` calls.
- `internal/core/dispatch_bench_test.go:19` — `if err := b.AddHandler(...); err != nil { panic(err) }`.
- `internal/core/dispatch_concurrency_test.go:28,34` — `if err := bot.AddHandler(...); err != nil`.

Fix: call `HandleFunc`/`AddHandler` as plain statements; to assert that a bad pattern was rejected, check the error from `bot.Connect(ctx)` instead. (As above, this depends on the registration API matching the retrieved context.)

Apply minimal, correct fixes that resolve these issues. Match the surrounding code's existing style and conventions, and do not change unrelated behavior.

Reviewed by prbooter. Comment /recheck to re-run, /security-review for a security audit, or /clean to remove my comments.

Comment thread internal/loadtest/loadtest.go Outdated
func (a *Adapter) Disconnect() error { return nil }

// Send records the call and discards the message.
func (a *Adapter) Send(_ context.Context, _, _ string) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[high] loadtest.Adapter does not satisfy core.Adapter — Send is missing the SendOptions parameter

Per the retrieved internal/core/core.go, the mandatory seam is:

type Adapter interface {
    Connect(ctx context.Context, deps AdapterDeps) error
    Disconnect() error
    Send(ctx context.Context, channelID, text string, opts SendOptions) error
    Attachments(m *Message) ([]Attachment, error)
}

This adapter's Send is declared Send(_ context.Context, _, _ string) error — three parameters, no opts SendOptions. That method set does not implement core.Adapter, so core.New(core.CLIBotType, a) on line 31 will not type-check and the whole loadtest package (and every test importing it) fails to build. internal/core/lifecycle_test.go's fakeAdapter.Send(_ context.Context, _, text string, _ SendOptions) error confirms the 4-arg shape.

Fix: func (a *Adapter) Send(_ context.Context, _, _ string, _ core.SendOptions) error.

(If the branch under review has a different core.Adapter.Send signature than the one retrieved here, disregard — but the retrieved interface and the SendOptions feature documented in CLAUDE.md make this very likely a real compile break.)

🤖 AI prompt to fix (review before running)
Fix an issue found during code review of lao/botbooter (PR #44).

File: internal/loadtest/loadtest.go:47  (side RIGHT)
Severity: high
Issue: loadtest.Adapter does not satisfy core.Adapter — Send is missing the SendOptions parameter

Per the retrieved `internal/core/core.go`, the mandatory seam is:

```go
type Adapter interface {
    Connect(ctx context.Context, deps AdapterDeps) error
    Disconnect() error
    Send(ctx context.Context, channelID, text string, opts SendOptions) error
    Attachments(m *Message) ([]Attachment, error)
}
```

This adapter's `Send` is declared `Send(_ context.Context, _, _ string) error` — three parameters, no `opts SendOptions`. That method set does not implement `core.Adapter`, so `core.New(core.CLIBotType, a)` on line 31 will not type-check and the whole `loadtest` package (and every test importing it) fails to build. `internal/core/lifecycle_test.go`'s `fakeAdapter.Send(_ context.Context, _, text string, _ SendOptions) error` confirms the 4-arg shape.

Fix: `func (a *Adapter) Send(_ context.Context, _, _ string, _ core.SendOptions) error`.

(If the branch under review has a different `core.Adapter.Send` signature than the one retrieved here, disregard — but the retrieved interface and the `SendOptions` feature documented in CLAUDE.md make this very likely a real compile break.)

Apply a minimal, correct fix that resolves this issue. Match the surrounding code's existing style and conventions, and do not change unrelated behavior.

Comment thread botbooter_soak_test.go Outdated
bot, a := loadtest.New()

var hits, miss atomic.Int64
asserts.NoError(t, bot.HandleFunc("^ping$", func(_ context.Context, _ *botbooter.Bot, _ *botbooter.Message) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[high] HandleFunc/AddHandler wrapped as error-returning, but registration returns no value

The retrieved core.go defines both registration helpers as returning nothing:

func (b *Bot) AddHandler(cmd Command) { ... }
func (b *Bot) HandleFunc(pattern string, handler CommandHandler) { b.AddHandler(...) }

and CLAUDE.md states "registration returns nothing, joebot-style: invalid patterns are recorded on the Bot and surface as one errors.Joined error from Connect/Run." Passing a no-value call as an argument (asserts.NoError(t, bot.HandleFunc(...), ...)) or binding it (if err := bot.AddHandler(...); err != nil) is a compile error.

This pattern appears across the new files:

  • botbooter_soak_test.go — the three asserts.NoError(t, bot.HandleFunc(...)) calls (this line and the two in TestSoak_PanicHandlerDoesNotCorruptCounts).
  • botbooter_overload_test.go — both asserts.NoError(t, bot.HandleFunc("^msg$", ...)) calls.
  • botbooter_endurance_test.go — both asserts.NoError(t, bot.HandleFunc("^endurance", ...)) calls.
  • internal/core/dispatch_bench_test.go:19if err := b.AddHandler(...); err != nil { panic(err) }.
  • internal/core/dispatch_concurrency_test.go:28,34if err := bot.AddHandler(...); err != nil.

Fix: call HandleFunc/AddHandler as plain statements; to assert that a bad pattern was rejected, check the error from bot.Connect(ctx) instead. (As above, this depends on the registration API matching the retrieved context.)

🤖 AI prompt to fix (review before running)
Fix an issue found during code review of lao/botbooter (PR #44).

File: botbooter_soak_test.go:24  (side RIGHT)
Severity: high
Issue: HandleFunc/AddHandler wrapped as error-returning, but registration returns no value

The retrieved `core.go` defines both registration helpers as returning nothing:

```go
func (b *Bot) AddHandler(cmd Command) { ... }
func (b *Bot) HandleFunc(pattern string, handler CommandHandler) { b.AddHandler(...) }
```

and `CLAUDE.md` states "registration returns nothing, joebot-style: invalid patterns are recorded on the Bot and surface as one `errors.Join`ed error from `Connect`/`Run`." Passing a no-value call as an argument (`asserts.NoError(t, bot.HandleFunc(...), ...)`) or binding it (`if err := bot.AddHandler(...); err != nil`) is a compile error.

This pattern appears across the new files:
- `botbooter_soak_test.go` — the three `asserts.NoError(t, bot.HandleFunc(...))` calls (this line and the two in `TestSoak_PanicHandlerDoesNotCorruptCounts`).
- `botbooter_overload_test.go` — both `asserts.NoError(t, bot.HandleFunc("^msg$", ...))` calls.
- `botbooter_endurance_test.go` — both `asserts.NoError(t, bot.HandleFunc("^endurance", ...))` calls.
- `internal/core/dispatch_bench_test.go:19` — `if err := b.AddHandler(...); err != nil { panic(err) }`.
- `internal/core/dispatch_concurrency_test.go:28,34` — `if err := bot.AddHandler(...); err != nil`.

Fix: call `HandleFunc`/`AddHandler` as plain statements; to assert that a bad pattern was rejected, check the error from `bot.Connect(ctx)` instead. (As above, this depends on the registration API matching the retrieved context.)

Apply a minimal, correct fix that resolves this issue. Match the surrounding code's existing style and conventions, and do not change unrelated behavior.

Resolve Makefile .PHONY conflict (union of both target lists) and update the
load-test suite to main's core API: Adapter.Send takes SendOptions, and
AddHandler/HandleFunc registration returns nothing. Switch the endurance smokes
to the per-platform slack.New/discord.New constructors.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (1)
internal/core/dispatch_bench_test.go (1)

15-32: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Benchmarks measure the uncached/never-connected dispatch path only.

benchBot never calls Connect, so dispatchChain is never populated and every benchmark here always takes dispatch's "no connection was ever established" branch, rebuilding the middleware/command closure chain on every call. A connected production bot instead reuses a cached chain (see composeDispatchChain/dispatchChain.Load() in internal/core/core.go). This is arguably the intent for BenchmarkDispatch_ScaleMiddleware (it explicitly targets chain-rebuild cost), but it silently taxes the other benchmarks too, and the doc comment at Line 82 generalizes it as core.dispatch's normal behavior rather than the fallback path specifically.

Consider adding a connected variant (e.g. wiring a trivial no-op core.Adapter and calling Connect before b.ResetTimer()) so at least one benchmark set reflects the cached hot path real bots run, and clarifying the doc comment to note the uncached-path caveat.

🤖 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 `@internal/core/dispatch_bench_test.go` around lines 15 - 32, Update benchBot
and the dispatch benchmarks to include a connected-bot variant that wires a
trivial no-op core.Adapter and calls Connect before timing, so at least one
benchmark measures the cached dispatchChain.Load() hot path. Keep the existing
uncached setup for the benchmark targeting chain-rebuild cost, and revise the
relevant benchmark documentation to identify it explicitly as the
never-connected fallback path rather than general dispatch behavior.
🤖 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 `@botbooter_endurance_test.go`:
- Around line 192-197: Remove the user-token branch from discordDriverAuth,
including the BOTBOOTER_DISCORD_DRIVER_USER_TOKEN environment lookup and
isUser=true return. Keep only the bot-token path using
BOTBOOTER_DISCORD_DRIVER_TOKEN, and update the function’s return contract or
callers as needed so the Discord driver smoke test no longer supports
user-account automation.
- Around line 82-89: Update enduranceDuration to distinguish an unset
environment variable from an explicit override: parse the configured value,
reject malformed or non-positive durations by failing fast, and only return the
two-minute default when the variable is unset. Preserve valid positive duration
overrides.

---

Nitpick comments:
In `@internal/core/dispatch_bench_test.go`:
- Around line 15-32: Update benchBot and the dispatch benchmarks to include a
connected-bot variant that wires a trivial no-op core.Adapter and calls Connect
before timing, so at least one benchmark measures the cached
dispatchChain.Load() hot path. Keep the existing uncached setup for the
benchmark targeting chain-rebuild cost, and revise the relevant benchmark
documentation to identify it explicitly as the never-connected fallback path
rather than general dispatch behavior.
🪄 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 Plus

Run ID: 7aa36296-664b-407b-bf97-884b1c10d680

📥 Commits

Reviewing files that changed from the base of the PR and between 2cbd005 and b5561dc.

📒 Files selected for processing (7)
  • Makefile
  • botbooter_endurance_test.go
  • botbooter_overload_test.go
  • botbooter_soak_test.go
  • internal/core/dispatch_bench_test.go
  • internal/core/dispatch_concurrency_test.go
  • internal/loadtest/loadtest.go

Comment thread botbooter_endurance_test.go Outdated
Comment on lines +82 to +89
func enduranceDuration(envName string, def time.Duration) time.Duration {
if v := os.Getenv(envName); v != "" {
if d, err := time.ParseDuration(v); err == nil {
return d
}
}
return def
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject invalid endurance-duration overrides.

A malformed or non-positive explicit value silently becomes the two-minute default, causing unintended live network testing. Fail fast instead.

Proposed fix
-func enduranceDuration(envName string, def time.Duration) time.Duration {
+func enduranceDuration(t *testing.T, envName string, def time.Duration) time.Duration {
+	t.Helper()
 	if v := os.Getenv(envName); v != "" {
-		if d, err := time.ParseDuration(v); err == nil {
-			return d
+		d, err := time.ParseDuration(v)
+		if err != nil || d <= 0 {
+			t.Fatalf("%s must be a positive Go duration, got %q", envName, v)
 		}
+		return d
 	}
 	return def
 }
 
-	duration: enduranceDuration("BOTBOOTER_SLACK_ENDURANCE_DURATION", 2*time.Minute),
+	duration: enduranceDuration(t, "BOTBOOTER_SLACK_ENDURANCE_DURATION", 2*time.Minute),
 
-	duration: enduranceDuration("BOTBOOTER_DISCORD_ENDURANCE_DURATION", 2*time.Minute),
+	duration: enduranceDuration(t, "BOTBOOTER_DISCORD_ENDURANCE_DURATION", 2*time.Minute),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func enduranceDuration(envName string, def time.Duration) time.Duration {
if v := os.Getenv(envName); v != "" {
if d, err := time.ParseDuration(v); err == nil {
return d
}
}
return def
}
func enduranceDuration(t *testing.T, envName string, def time.Duration) time.Duration {
t.Helper()
if v := os.Getenv(envName); v != "" {
d, err := time.ParseDuration(v)
if err != nil || d <= 0 {
t.Fatalf("%s must be a positive Go duration, got %q", envName, v)
}
return d
}
return def
}
🤖 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 `@botbooter_endurance_test.go` around lines 82 - 89, Update enduranceDuration
to distinguish an unset environment variable from an explicit override: parse
the configured value, reject malformed or non-positive durations by failing
fast, and only return the two-minute default when the variable is unset.
Preserve valid positive duration overrides.

Comment on lines +192 to +197
func discordDriverAuth(t *testing.T) (auth string, isUser bool) {
t.Helper()
if userToken := os.Getenv("BOTBOOTER_DISCORD_DRIVER_USER_TOKEN"); userToken != "" {
return userToken, true
}
return "Bot " + requireEnv(t, "BOTBOOTER_DISCORD_DRIVER_TOKEN"), false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove Discord user-account automation.

BOTBOOTER_DISCORD_DRIVER_USER_TOKEN enables a path this file identifies as violating Discord’s Terms of Service. An opt-in flag does not remove the account or compliance risk; retain only the bot-token connection smoke.

Proposed fix
-func discordDriverAuth(t *testing.T) (auth string, isUser bool) {
+func discordDriverAuth(t *testing.T) string {
 	t.Helper()
-	if userToken := os.Getenv("BOTBOOTER_DISCORD_DRIVER_USER_TOKEN"); userToken != "" {
-		return userToken, true
-	}
-	return "Bot " + requireEnv(t, "BOTBOOTER_DISCORD_DRIVER_TOKEN"), false
+	return "Bot " + requireEnv(t, "BOTBOOTER_DISCORD_DRIVER_TOKEN")
 }
🤖 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 `@botbooter_endurance_test.go` around lines 192 - 197, Remove the user-token
branch from discordDriverAuth, including the BOTBOOTER_DISCORD_DRIVER_USER_TOKEN
environment lookup and isUser=true return. Keep only the bot-token path using
BOTBOOTER_DISCORD_DRIVER_TOKEN, and update the function’s return contract or
callers as needed so the Discord driver smoke test no longer supports
user-account automation.

@lao

lao commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

/review

@botbooter-test

botbooter-test Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

✅ Review posted — 4 finding(s). b5561dcc

  • Fetch changed files
  • Build repository context
  • Generate review
  • Post findings

@botbooter-test botbooter-test Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗂️ Old review — new review in flight

Verdict: comment · Grade: 6/10

To reach a higher grade: Make the benchmarks measure the production dispatch path (Connect so the stored chain is used) and fix the middleware-rebuild comment that contradicts core.go.

📝 Summary of changes

What this changes

Test-only PR adding load/soak/overload coverage plus benchmarks:

  • internal/loadtest — an in-memory core.Adapter (Pump, Gauge, AssertNoGoroutineLeak) that captures deps.Dispatch at Connect and drives synthetic traffic through the real pipeline. The CAS loop in Gauge.Enter is correct, the Pump work-stealing counter is correct, and the happens-before argument for the unsynchronized a.dispatch write (written on the test goroutine before workers are spawned) holds.
  • botbooter_soak_test.go / botbooter_overload_test.go — exact-count and concurrency-shape assertions. The barrier in TestOverload_ConcurrentDriver_NoBackpressure (total == workers, handlers park on release) makes the peak assertion deterministic rather than timing-based; Peak() can never exceed workers, so the exact equality is safe.
  • botbooter_endurance_test.go — env-gated real-platform smokes, following the existing TestConnectSlack_StartsAndStops pattern. The Discord user-token mode is a documented, explicit opt-in with the ToS caveat spelled out, which is a reasonable way to handle it.
  • internal/core/dispatch_bench_test.go and Makefile targets.

Assessment

The concurrency reasoning is generally sound and the tests look flake-resistant. The main problem is that the benchmarks (and the new core concurrency test) construct a Bot and call dispatch without ever calling Connect, so they exercise core.dispatch's never-connected fallback (b.composeDispatchChain()(...) per call) instead of the production path that reads the chain composed once at Connect and stored in dispatchChain. That invalidates BenchmarkDispatch_ScaleMiddleware entirely and bakes an incorrect claim about core's behavior into a comment.

Smaller items: the endurance helper swallows a time.ParseDuration error, the receipt assertion is vacuous when sent == 0, and README/CLAUDE.md still state there is exactly one env-gated network test and list the old make targets.

No prompt-injection or steering text found in the diff; the CodeRabbit block is a normal release-notes summary.

🤖 AI prompt to fix all 4 finding(s) (review before running)
Fix 4 issue(s) found during code review of lao/botbooter (PR #44).

--- Issue 1 ---
File: internal/core/dispatch_bench_test.go:16  (side RIGHT)
Severity: medium
Issue: Benchmarks never Connect, so they measure the compose-per-dispatch fallback, not the production path

`benchBot` builds the Bot with `New(CLIBotType, nil)` and never calls `Connect`, so `dispatchChain` is nil and every `bot.dispatch` call falls into the documented fallback branch in `core.go`:

```go
// No connection was ever established (e.g. a unit test calling dispatch
// directly); compose on the fly from the current middleware.
b.composeDispatchChain()(ctx, b, message)
```

In production the chain is composed **once per Connect** and stored atomically (`Connect`: `chain := b.composeDispatchChain(); b.dispatchChain.Store(&chain)`), so no message pays for the rebuild. Consequences:

- `BenchmarkDispatch_ScaleMiddleware`'s stated purpose — "quantifies the per-dispatch middleware-chain rebuild (core.dispatch reconstructs the closure chain on every call)" (lines 81–83) — is measuring a path production never takes, and the comment itself contradicts `core.go`. Anyone reading the resulting allocs/op would conclude middleware costs N allocations per message when it costs zero after Connect.
- The command-scan and parallel benchmarks are also inflated by a per-iteration chain composition.

Suggestion: take the `*testing.B`, wire the existing in-package `stubAdapter` and connect, e.g.

```go
func benchBot(b *testing.B, numCommands, numMiddleware int, matchFirst bool) (*Bot, *Message) {
	b.Helper()
	bot := New(CLIBotType, &stubAdapter{})
	// ... registrations ...
	if err := bot.Connect(context.Background()); err != nil {
		b.Fatal(err)
	}
	b.Cleanup(func() { _ = bot.Disconnect() })
	...
}
```

and rewrite the `ScaleMiddleware` comment to say it measures the *per-message* cost of traversing an N-deep composed chain. (Same note applies to `internal/core/dispatch_concurrency_test.go:24`, which also dispatches on a never-connected Bot; connecting there would exercise the atomic-chain read that production actually performs.)

--- Issue 2 ---
File: botbooter_endurance_test.go:84  (side RIGHT)
Severity: low
Issue: A malformed duration env var is silently ignored

`enduranceDuration` drops the `time.ParseDuration` error and falls back to the default, so `BOTBOOTER_SLACK_ENDURANCE_DURATION=5min` (or any typo) silently runs the 2-minute default and the operator never learns their configuration was ignored — while `make endurance`'s 15m timeout comment implies the value matters. Every other env var in this file is validated via `requireEnv`.

Suggestion: pass `t` and fail loudly:

```go
func enduranceDuration(t *testing.T, envName string, def time.Duration) time.Duration {
	t.Helper()
	v := os.Getenv(envName)
	if v == "" {
		return def
	}
	d, err := time.ParseDuration(v)
	if err != nil {
		t.Fatalf("%s=%q: %v", envName, v, err)
	}
	return d
}
```

--- Issue 3 ---
File: botbooter_endurance_test.go:131  (side RIGHT)
Severity: low
Issue: Receipt assertion passes vacuously when no messages were driven

`recv.Load() >= int64(sent/2)` is trivially true when `sent == 0`, which happens if the configured duration is shorter than the tick interval (or if `enduranceDuration` silently fell back after a parse error). The test would then report success having proven nothing about message flow. The same applies to the Discord user-driver branch (line 174).

Suggestion: assert the driver actually ran, e.g. add `asserts.True(t, sent > 0, "driver posted at least one message")` before the receipt check, or enforce a minimum inside `runEnduranceSmoke` (`if sent == 0 { t.Fatal("no drive calls made; duration shorter than interval?") }`).

--- Issue 4 ---
File: Makefile:37  (side RIGHT)
Severity: low
Issue: New make targets and env-gated network tests not reflected in README/CLAUDE.md

This repo keeps its make targets and env-gated tests documented in both `README.md` (Development section) and `CLAUDE.md` (Commands section). Both currently assert there is exactly one opt-in network test:

- README: *"The single test that touches the Slack network is opt-in, enabled by setting the `BOTBOOTER_SLACK_NETWORK_TEST` environment variable"*
- CLAUDE.md: *"The suite is hermetic by default. `TestConnectSlack_StartsAndStops` does real Slack network I/O and is skipped unless `BOTBOOTER_SLACK_NETWORK_TEST` is set."*

After this PR both statements are stale, and `bench` / `soak` / `endurance` plus the six new `BOTBOOTER_{SLACK,DISCORD}_*` variables are undocumented outside the Makefile comments. Suggestion: add the three targets to the command lists and extend the hermeticity note to mention the endurance smokes and their env vars.

Apply minimal, correct fixes that resolve these issues. Match the surrounding code's existing style and conventions, and do not change unrelated behavior.

Reviewed by prbooter. Comment /recheck to re-run, /security-review for a security audit, or /clean to remove my comments.

Comment thread internal/core/dispatch_bench_test.go Outdated
// otherwise it matches none, so dispatch scans every command and falls through
// to the unknown-command handler (worst case for the linear scan).
func benchBot(numCommands, numMiddleware int, matchFirst bool) (*Bot, *Message) {
b := New(CLIBotType, nil)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[medium] Benchmarks never Connect, so they measure the compose-per-dispatch fallback, not the production path

benchBot builds the Bot with New(CLIBotType, nil) and never calls Connect, so dispatchChain is nil and every bot.dispatch call falls into the documented fallback branch in core.go:

// No connection was ever established (e.g. a unit test calling dispatch
// directly); compose on the fly from the current middleware.
b.composeDispatchChain()(ctx, b, message)

In production the chain is composed once per Connect and stored atomically (Connect: chain := b.composeDispatchChain(); b.dispatchChain.Store(&chain)), so no message pays for the rebuild. Consequences:

  • BenchmarkDispatch_ScaleMiddleware's stated purpose — "quantifies the per-dispatch middleware-chain rebuild (core.dispatch reconstructs the closure chain on every call)" (lines 81–83) — is measuring a path production never takes, and the comment itself contradicts core.go. Anyone reading the resulting allocs/op would conclude middleware costs N allocations per message when it costs zero after Connect.
  • The command-scan and parallel benchmarks are also inflated by a per-iteration chain composition.

Suggestion: take the *testing.B, wire the existing in-package stubAdapter and connect, e.g.

func benchBot(b *testing.B, numCommands, numMiddleware int, matchFirst bool) (*Bot, *Message) {
	b.Helper()
	bot := New(CLIBotType, &stubAdapter{})
	// ... registrations ...
	if err := bot.Connect(context.Background()); err != nil {
		b.Fatal(err)
	}
	b.Cleanup(func() { _ = bot.Disconnect() })
	...
}

and rewrite the ScaleMiddleware comment to say it measures the per-message cost of traversing an N-deep composed chain. (Same note applies to internal/core/dispatch_concurrency_test.go:24, which also dispatches on a never-connected Bot; connecting there would exercise the atomic-chain read that production actually performs.)

🤖 AI prompt to fix (review before running)
Fix an issue found during code review of lao/botbooter (PR #44).

File: internal/core/dispatch_bench_test.go:16  (side RIGHT)
Severity: medium
Issue: Benchmarks never Connect, so they measure the compose-per-dispatch fallback, not the production path

`benchBot` builds the Bot with `New(CLIBotType, nil)` and never calls `Connect`, so `dispatchChain` is nil and every `bot.dispatch` call falls into the documented fallback branch in `core.go`:

```go
// No connection was ever established (e.g. a unit test calling dispatch
// directly); compose on the fly from the current middleware.
b.composeDispatchChain()(ctx, b, message)
```

In production the chain is composed **once per Connect** and stored atomically (`Connect`: `chain := b.composeDispatchChain(); b.dispatchChain.Store(&chain)`), so no message pays for the rebuild. Consequences:

- `BenchmarkDispatch_ScaleMiddleware`'s stated purpose — "quantifies the per-dispatch middleware-chain rebuild (core.dispatch reconstructs the closure chain on every call)" (lines 81–83) — is measuring a path production never takes, and the comment itself contradicts `core.go`. Anyone reading the resulting allocs/op would conclude middleware costs N allocations per message when it costs zero after Connect.
- The command-scan and parallel benchmarks are also inflated by a per-iteration chain composition.

Suggestion: take the `*testing.B`, wire the existing in-package `stubAdapter` and connect, e.g.

```go
func benchBot(b *testing.B, numCommands, numMiddleware int, matchFirst bool) (*Bot, *Message) {
	b.Helper()
	bot := New(CLIBotType, &stubAdapter{})
	// ... registrations ...
	if err := bot.Connect(context.Background()); err != nil {
		b.Fatal(err)
	}
	b.Cleanup(func() { _ = bot.Disconnect() })
	...
}
```

and rewrite the `ScaleMiddleware` comment to say it measures the *per-message* cost of traversing an N-deep composed chain. (Same note applies to `internal/core/dispatch_concurrency_test.go:24`, which also dispatches on a never-connected Bot; connecting there would exercise the atomic-chain read that production actually performs.)

Apply a minimal, correct fix that resolves this issue. Match the surrounding code's existing style and conventions, and do not change unrelated behavior.

Comment thread botbooter_endurance_test.go Outdated

func enduranceDuration(envName string, def time.Duration) time.Duration {
if v := os.Getenv(envName); v != "" {
if d, err := time.ParseDuration(v); err == nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[low] A malformed duration env var is silently ignored

enduranceDuration drops the time.ParseDuration error and falls back to the default, so BOTBOOTER_SLACK_ENDURANCE_DURATION=5min (or any typo) silently runs the 2-minute default and the operator never learns their configuration was ignored — while make endurance's 15m timeout comment implies the value matters. Every other env var in this file is validated via requireEnv.

Suggestion: pass t and fail loudly:

func enduranceDuration(t *testing.T, envName string, def time.Duration) time.Duration {
	t.Helper()
	v := os.Getenv(envName)
	if v == "" {
		return def
	}
	d, err := time.ParseDuration(v)
	if err != nil {
		t.Fatalf("%s=%q: %v", envName, v, err)
	}
	return d
}
🤖 AI prompt to fix (review before running)
Fix an issue found during code review of lao/botbooter (PR #44).

File: botbooter_endurance_test.go:84  (side RIGHT)
Severity: low
Issue: A malformed duration env var is silently ignored

`enduranceDuration` drops the `time.ParseDuration` error and falls back to the default, so `BOTBOOTER_SLACK_ENDURANCE_DURATION=5min` (or any typo) silently runs the 2-minute default and the operator never learns their configuration was ignored — while `make endurance`'s 15m timeout comment implies the value matters. Every other env var in this file is validated via `requireEnv`.

Suggestion: pass `t` and fail loudly:

```go
func enduranceDuration(t *testing.T, envName string, def time.Duration) time.Duration {
	t.Helper()
	v := os.Getenv(envName)
	if v == "" {
		return def
	}
	d, err := time.ParseDuration(v)
	if err != nil {
		t.Fatalf("%s=%q: %v", envName, v, err)
	}
	return d
}
```

Apply a minimal, correct fix that resolves this issue. Match the surrounding code's existing style and conventions, and do not change unrelated behavior.

})

t.Logf("slack endurance: sent %d, received %d", sent, recv.Load())
asserts.True(t, recv.Load() >= int64(sent/2), "bot kept receiving messages across the window")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[low] Receipt assertion passes vacuously when no messages were driven

recv.Load() >= int64(sent/2) is trivially true when sent == 0, which happens if the configured duration is shorter than the tick interval (or if enduranceDuration silently fell back after a parse error). The test would then report success having proven nothing about message flow. The same applies to the Discord user-driver branch (line 174).

Suggestion: assert the driver actually ran, e.g. add asserts.True(t, sent > 0, "driver posted at least one message") before the receipt check, or enforce a minimum inside runEnduranceSmoke (if sent == 0 { t.Fatal("no drive calls made; duration shorter than interval?") }).

🤖 AI prompt to fix (review before running)
Fix an issue found during code review of lao/botbooter (PR #44).

File: botbooter_endurance_test.go:131  (side RIGHT)
Severity: low
Issue: Receipt assertion passes vacuously when no messages were driven

`recv.Load() >= int64(sent/2)` is trivially true when `sent == 0`, which happens if the configured duration is shorter than the tick interval (or if `enduranceDuration` silently fell back after a parse error). The test would then report success having proven nothing about message flow. The same applies to the Discord user-driver branch (line 174).

Suggestion: assert the driver actually ran, e.g. add `asserts.True(t, sent > 0, "driver posted at least one message")` before the receipt check, or enforce a minimum inside `runEnduranceSmoke` (`if sent == 0 { t.Fatal("no drive calls made; duration shorter than interval?") }`).

Apply a minimal, correct fix that resolves this issue. Match the surrounding code's existing style and conventions, and do not change unrelated behavior.

Comment thread Makefile

# Gated real-platform endurance smokes; skipped unless the BOTBOOTER_{SLACK,DISCORD}_*
# env vars are exported. Timeout must exceed the configured endurance duration.
endurance:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[low] New make targets and env-gated network tests not reflected in README/CLAUDE.md

This repo keeps its make targets and env-gated tests documented in both README.md (Development section) and CLAUDE.md (Commands section). Both currently assert there is exactly one opt-in network test:

  • README: "The single test that touches the Slack network is opt-in, enabled by setting the BOTBOOTER_SLACK_NETWORK_TEST environment variable"
  • CLAUDE.md: "The suite is hermetic by default. TestConnectSlack_StartsAndStops does real Slack network I/O and is skipped unless BOTBOOTER_SLACK_NETWORK_TEST is set."

After this PR both statements are stale, and bench / soak / endurance plus the six new BOTBOOTER_{SLACK,DISCORD}_* variables are undocumented outside the Makefile comments. Suggestion: add the three targets to the command lists and extend the hermeticity note to mention the endurance smokes and their env vars.

🤖 AI prompt to fix (review before running)
Fix an issue found during code review of lao/botbooter (PR #44).

File: Makefile:37  (side RIGHT)
Severity: low
Issue: New make targets and env-gated network tests not reflected in README/CLAUDE.md

This repo keeps its make targets and env-gated tests documented in both `README.md` (Development section) and `CLAUDE.md` (Commands section). Both currently assert there is exactly one opt-in network test:

- README: *"The single test that touches the Slack network is opt-in, enabled by setting the `BOTBOOTER_SLACK_NETWORK_TEST` environment variable"*
- CLAUDE.md: *"The suite is hermetic by default. `TestConnectSlack_StartsAndStops` does real Slack network I/O and is skipped unless `BOTBOOTER_SLACK_NETWORK_TEST` is set."*

After this PR both statements are stale, and `bench` / `soak` / `endurance` plus the six new `BOTBOOTER_{SLACK,DISCORD}_*` variables are undocumented outside the Makefile comments. Suggestion: add the three targets to the command lists and extend the hermeticity note to mention the endurance smokes and their env vars.

Apply a minimal, correct fix that resolves this issue. Match the surrounding code's existing style and conventions, and do not change unrelated behavior.

core.dispatch composes the middleware chain per call only when the Bot
was never connected; Connect composes it once and publishes it in an
atomic.Pointer. Benchmarking an unconnected Bot therefore measured a
path no production message takes, reporting allocs/op that scale with
the middleware count where the real figure is zero (mw=10: 242ns and 10
allocs unconnected, 52ns and 0 allocs connected).

- benchBot and TestDispatch_ConcurrentReads_RaceFree connect through
  stubAdapter, so both exercise the shared chain production reads
- endurance: fail loudly on an unparseable *_ENDURANCE_DURATION and on
  zero drive calls, and require recv > 0 so the receipt check cannot
  pass vacuously; retire idle keep-alives before the goroutine-leak
  check, which otherwise counts pooled net/http readLoop/writeLoop
  goroutines as leaks for the whole IdleConnTimeout
- loadtest: drop the unused Sends and Dispatch accessors, take
  asserts.TestingT rather than import testing into a non-test package,
  and drop the runtime.GC call that never affected goroutine lifetime
- soak and endurance run with -v, since their point is the logged
  throughput and sent/received tallies
- README and CLAUDE.md: document the bench, soak and endurance targets
  and the three env-gated network tests
@lao

lao commented Jul 26, 2026

Copy link
Copy Markdown
Owner Author

/loop

@botbooter-test

Copy link
Copy Markdown
Contributor

🔁 Review-fix loop started — target 9/10, up to 3 fix round(s).

@botbooter-test

botbooter-test Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

✅ No issues found. dd2896d9

  • Fetch changed files
  • Build repository context
  • Generate review
  • Post findings

@botbooter-test botbooter-test Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: approve · Grade: 9/10

📝 Summary of changes

This is a test-and-tooling-only PR adding load/soak/overload/endurance testing infrastructure for botbooter's dispatch path, plus a new internal/loadtest in-memory adapter, benchmarks, three make targets (bench, soak, endurance), and doc updates in CLAUDE.md/README.md.

The work is careful and well-reasoned:

  • The loadtest.Adapter correctly captures deps.Dispatch at Connect and drives the real command/middleware pipeline with no network I/O; the happens-before argument for the unsynchronized dispatch field write (test goroutine writes before go spawns workers) is sound.
  • The soak/concurrency tests deliberately Connect the Bot first so they exercise the published atomic.Pointer dispatch chain rather than the compose-on-the-fly fallback, matching the documented production path; exact hit/miss counts prove no lost/double dispatch under -race.
  • The overload tests use deterministic channel barriers (release, waitForPeak) rather than wall-clock timing, and the Gauge CAS loop is a correct concurrent max tracker.
  • The endurance smokes are env-gated exactly like the existing TestConnectSlack_StartsAndStops, retire idle keep-alive connections before the goroutine-leak check, and keep total runtime well under the 15m target timeout.

Doc/Makefile changes are consistent with existing conventions (internal/asserts, env-var gating, -race gate). I found no correctness, concurrency, or API-misuse issues. The timing-based AssertNoGoroutineLeak polling and waitForPeak deadline use generous windows in line with the repo's existing patterns. The endurance-test sent++ incrementing even on a failed drive is a theoretical flakiness source for the recv >= sent/2 bound, but only in the opt-in, manually-run network path, so it is not worth blocking on.

✅ No issues found.


Reviewed by prbooter. Comment /recheck to re-run, /security-review for a security audit, /auto-fix to have me attempt the fixes, or /clean to remove my comments.

@botbooter-test

Copy link
Copy Markdown
Contributor

🔁 Review-fix loop finished — target 9/10.

  • Reviews run: 1
  • Fix commits pushed: 0
  • Grade trajectory: 9
  • Stopped: target reached

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.

1 participant