Skip to content

Share test fakes across packages and fix the bugs they hid - #235

Merged
trevor-vaughan merged 7 commits into
mainfrom
feature/test-dry-sweep
Oct 27, 2026
Merged

trevor-vaughan merged 7 commits into
mainfrom
feature/test-dry-sweep

Conversation

@trevor-vaughan

Copy link
Copy Markdown
Collaborator

🦾 Written with LLM assistance [claude-opus-5-5]
💪 Reviewed by a human before submission


Summary

Each package had its own hand-rolled fakes for db, NATS, the LLM client, storage and the other pkg/ interfaces. Over time they drifted apart. Some panicked on a call nobody scripted, some returned nil, some silently succeeded, and several accepted input the real implementation rejects. This PR replaces them with one shared fake per interface. Moving to the shared fakes exposed two production bugs, and this PR fixes both:

  • Collector retries never worked. The analysis collector passed its NATS handler context to Dispatcher.Redispatch. That context never carries a tenant, so every retry failed with ErrNoTenant and showed up as ErrRetryExhausted.
  • The gateway leaked internal error detail (CWE-209). Handlers returned backend errors unchanged, so clients could see SQL, role names, file paths and dial addresses.

The PR also collapses duplicated specs into DescribeTable tables, removes the slowest unit specs, and writes the resulting conventions into AGENTS.md.

Changes

Shared test fakes (a2d6f35)

  • Add one fake package per interface: dbtest, llmclienttest, natsbustest, storagetest, analyzertest, prompttest, vectordbtest, retentiontest. All of them work the same way:
    • Behavior is scripted through <Method>Func fields.
    • A call nobody scripted returns ErrUnscripted, which names the field to set. It never panics.
    • Recorded calls are deep copies.
    • Input that production rejects is rejected.
  • Each fake package has its own bdd and property specs. retentiontest.HoldStoreContract runs against both the fake and the Postgres hold store, so the two cannot drift apart.
  • DeterministicLLM moves to internal/testspecs.

Fix: collector redispatch loses the tenant (f644e37)

  • The collector now rebuilds the tenant context from the subject tenant, which it has already validated, before it calls Redispatch.
  • natsbustest.Fake.Deliver and Sub.Handle now give handlers a context without a tenant, the same as natsbus wrapHandler does in production. The old fake passed the caller's context through, which is why no spec caught this bug.

Fix: gateway withholds unclassified errors (360f15c)

  • publicError (internal/gateway/helpers.go) is an allowlist. Every GatewayService handler calls it, and an interceptor applies it to the AdminService mount.
    • Codes the caller can act on pass through unchanged: InvalidArgument, NotFound, AlreadyExists, FailedPrecondition, OutOfRange, PermissionDenied, Unauthenticated, ResourceExhausted, Canceled and Unimplemented.
    • Unavailable, DeadlineExceeded and Aborted keep their code so client retry logic still works, but the message is replaced.
    • Everything else, including plain errors, becomes Internal.
    • When a message is withheld, the server logs the full error once under a new error reference and returns only that reference to the client. The reference is generated by the server, not taken from the trace ID, because the caller controls the trace ID through traceparent.
  • rpcserver's panic recovery returns the same kind of error reference.
  • The catalog service used to decide "not found" by matching error text, and it reported every storage failure as NotFound. That leaked storage detail through the allowlist. It now uses sentinel errors with errors.Is.
  • A client certificate that names an invalid tenant ID now gets Unauthenticated with an actionable message instead of Internal. The refusal is also counted in the auth-failure metric and logged.

Test cleanup (1611fb6)

  • Collapse near-identical It blocks into DescribeTable/Entry. Every assertion is kept.
  • Add dbtest.RoleDSN, SetRolePasswordFromEnv and TestID so integration suites stop copying the same role-DSN and password setup. RoleDSN refuses a malformed DSN without echoing it, because url.Parse errors quote the superuser password. The migrate integration test also stops printing url.Parse errors for TEST_DATABASE_DSN.
  • Remove two specs that took most of the unit run: one generated an RSA key on every iteration, and one ran an unbounded memdriver traversal.
  • Specs that send empty arguments to the gateway now assert that no backend is reached, so each entry fails if the validation is removed.
  • Replace fixed sleeps in the natsbus integration specs with shared wait helpers, and fix a race in a metrics wait.
  • The pkg/db tenant-scope source walk now skips dot-directories. It used to fail when a concurrent build removed a .go-tmp directory.

Docs (7dc6353)

  • AGENTS.md has a new "Test doubles and fixtures" section. It lists the shared fake for each interface, says when a local fake is still allowed, explains how to embed a fake and override only some methods, and requires contract specs for stateful fakes.
  • AGENTS.md also adds rules for table entries, keeping expensive fixtures out of rapid.Check bodies, bounding traversals with MaxDepth, and using Eventually/Consistently instead of time.Sleep. It now requires publicError for gateway dependency errors and sentinel errors instead of matching error text.
  • Fix stale doc.go notes in the fake packages, for example the gaps in dbtest and the "deep copy" claim for vectordbtest.Stored.

Client-visible changes

  • Gateway errors with code Unknown, Internal or DataLoss, and plain errors, now reach clients as Internal with an opaque message and an error reference. Unavailable, DeadlineExceeded and Aborted keep their code, but their message is replaced the same way. To find the cause, search the gateway logs for the error_ref value.
  • A client certificate that names an invalid tenant ID now returns Unauthenticated instead of Internal.

Testing

  • task check (lint, unit, race)
  • Integration suites that use the new dbtest helpers: task test:integration:<name>
  • Fuzz targets were not run locally. CI fuzzes the changed packages.

🤖 Generated with Claude Code

Each package hand-rolled its own fakes for db, NATS, LLM client,
storage, analyzer, prompt registry, vectordb and retention hold store.
They had drifted apart: some panicked on an unscripted call, some
returned nil, some silently succeeded, and several accepted inputs the
real implementation rejects. That hid real bugs.

Each interface now has one shared fake in a <pkg>test package (dbtest,
llmclienttest, natsbustest, storagetest, analyzertest, prompttest,
vectordbtest, retentiontest). They follow one convention: <Method>Func
fields, an ErrUnscripted error naming the field to set (never a panic),
deep-copied recorders, and rejection of what production rejects. The
retention fake runs the same HoldStoreContract as the Postgres store
so it cannot drift from it. DeterministicLLM moves to testspecs.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Trevor Vaughan <peiriannydd@gmail.com>
The collector's result handler passed its handler context to
Dispatcher.Redispatch. pkg/natsbus wrapHandler builds that context from
context.Background() plus a span, so it never carries a tenant, and
NATSDispatcher.Redispatch requires one. In production every retry
failed with ErrNoTenant and the task was reported as ErrRetryExhausted.
The collector now rebuilds the tenant context from the validated
subject tenant.

natsbustest.Fake.Deliver and Sub.Handle passed the caller's context to
handlers, so specs saw a tenant that production never provides. That is
what hid this bug. They now deliver with a tenantless context, as the
real wrapHandler does.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Trevor Vaughan <peiriannydd@gmail.com>
Many suites repeated near-identical It blocks that differed only in
inputs, and integration suites copied the same role-DSN and password
bootstrap code. Collapse them into DescribeTable/Entry, keeping every
assertion and spec count, and add dbtest.RoleDSN,
SetRolePasswordFromEnv and TestID so integration suites share one
implementation. RoleDSN refuses malformed DSNs without echoing them,
because url.Parse errors quote the superuser password.

Along the way:
- Remove two pathological slow specs (per-iteration RSA keygen and an
  unbounded memdriver traversal) that took most of the unit run.
- Make empty-argument gateway specs assert that no backend is reached,
  so each entry fails if validation is removed.
- Replace fixed sleeps in natsbus integration specs with shared helpers
  and fix a metrics wait race.
- Skip dot-directories in the pkg/db tenant-scope source walk, which
  failed when a concurrent build removed a .go-tmp directory.
- Do not print url.Parse errors for TEST_DATABASE_DSN in the migrate
  integration test.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Trevor Vaughan <peiriannydd@gmail.com>
Every gateway handler returned a backend error as-is, so driver detail
(SQL, role names, paths, dial addresses) reached clients as
CodeUnknown or through codes like Unavailable (CWE-209).

publicError is now an allowlist applied to every handler and the
AdminService mount. Only caller-actionable codes pass through
unchanged. Everything else is logged once and replaced by an opaque
message naming a server-generated error reference, not the trace ID:
the caller can set the trace ID via traceparent. Retryable codes keep
their code so client retry logic still works. rpcserver's panic
recovery uses the same reference.

The catalog service classified not-found by raw error text and turned
every storage failure into NotFound, which leaked storage detail
through the allowlist. It now uses sentinels. The auth interceptor now
counts and logs certificates refused for an invalid tenant ID instead
of refusing them silently.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Trevor Vaughan <peiriannydd@gmail.com>
- docs(agents): add a "Test doubles and fixtures" section
  - Lists the shared fake package for each pkg/ interface and when a local fake is still allowed
  - Covers when to embed a fake and override only some methods, the contract specs stateful fakes need, and shared fixtures
  - The sweep found that hand-rolled fakes hid real bugs, so new tests should use the shared fakes
- docs(agents): add table, rapid.Check speed and no-sleep rules for tests
- docs(agents): require publicError for gateway dependency errors
  - Also: classify errors with sentinels and errors.Is, never by matching message text
- docs(gateway): say publicError points at an error reference, not the trace
- docs(testfakes): fix stale notes in the shared fake package docs
  - dbtest documents gaps: no use-after-Commit check, and Scan rejects unsupported destinations
  - vectordbtest no longer says Stored returns a deep copy

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Trevor Vaughan <peiriannydd@gmail.com>
@trevor-vaughan trevor-vaughan added the llm-assisted Created with LLM assistance label Oct 27, 2026
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

✅⚠️MegaLinter analysis: Success with warnings

Descriptor Linter Files Fixed Errors Max errors Warnings Elapsed time
✅ ACTION actionlint 2 0 0 0.41s
✅ ACTION zizmor 2 0 0 1.31s
✅ COPYPASTE jscpd yes no no 1.33s
✅ GO golangci-lint yes no no 98.01s
⚠️ MARKDOWN markdownlint 2 12 0 0.64s
⚠️ MARKDOWN markdown-table-formatter 2 1 0 0.16s
✅ YAML prettier 3 0 0 0.33s
✅ YAML yamllint 3 0 0 0.36s

Detailed Issues

⚠️ MARKDOWN / markdown-table-formatter - 1 error
2 files contain markdown tables to format:
- README.md
- docs/dev/design-principles.md
⚠️ MARKDOWN / markdownlint - 12 errors
docs/dev/design-principles.md:21:9 error MD060/table-column-style Table column style [Table pipe is missing space to the left for style "compact"]
docs/dev/design-principles.md:21:16 error MD060/table-column-style Table column style [Table pipe is missing space to the left for style "compact"]
docs/dev/design-principles.md:21:22 error MD060/table-column-style Table column style [Table pipe is missing space to the left for style "compact"]
docs/dev/design-principles.md:21:1 error MD060/table-column-style Table column style [Table pipe is missing space to the right for style "compact"]
docs/dev/design-principles.md:21:9 error MD060/table-column-style Table column style [Table pipe is missing space to the right for style "compact"]
docs/dev/design-principles.md:21:16 error MD060/table-column-style Table column style [Table pipe is missing space to the right for style "compact"]
README.md:116:8 error MD060/table-column-style Table column style [Table pipe is missing space to the left for style "compact"]
README.md:116:21 error MD060/table-column-style Table column style [Table pipe is missing space to the left for style "compact"]
README.md:116:31 error MD060/table-column-style Table column style [Table pipe is missing space to the left for style "compact"]
README.md:116:1 error MD060/table-column-style Table column style [Table pipe is missing space to the right for style "compact"]
README.md:116:8 error MD060/table-column-style Table column style [Table pipe is missing space to the right for style "compact"]
README.md:116:21 error MD060/table-column-style Table column style [Table pipe is missing space to the right for style "compact"]

Notices

⚠️ Your configuration references items that have been removed from MegaLinter and are ignored: API_SPECTRAL, MAKEFILE_CHECKMAKE, MARKDOWN_MARKDOWN_LINK_CHECK, REPOSITORY_GITLEAKS, REPOSITORY_KICS. See Removed linters to find their replacements.

See detailed reports in MegaLinter artifacts
Set VALIDATE_ALL_CODEBASE: true in mega-linter.yml to validate all sources, not only the diff

MegaLinter is provided by OX Security
Show us your support by starring ⭐ the repository

Ginkgo's --timeout includes compiling the suite with --race and
coverage. On a slow CI runner the compile alone exceeded 120s, so
integration-grpc on PR #235 failed with "Suite did not run because the
timeout elapsed" and zero specs run. 300s is the existing tier used by
the other suites.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Trevor Vaughan <peiriannydd@gmail.com>
Scoping PR fuzzing to changed packages still took too long for a PR
workflow: a broad PR touches most packages, and each target pays a
fixed build cost on top of its 30s. A shorter FUZZ_TIME would cut the
fuzzing but not the build, and would add little over the seed corpora
and committed crash inputs that `task test` already replays.

fuzz-nightly.yml remains the one place that fuzzes. Drop fetch-depth: 2,
which only the PR fuzz step needed, and record the decision in the
workflows, README and AGENTS.md so it is not re-added by accident.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Trevor Vaughan <peiriannydd@gmail.com>
@trevor-vaughan
trevor-vaughan marked this pull request as ready for review October 27, 2026 21:01
@trevor-vaughan
trevor-vaughan merged commit 85892db into main Oct 27, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

llm-assisted Created with LLM assistance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant