Skip to content

v3 - #16

Merged
lirbank merged 42 commits into
mainfrom
v3
Jun 16, 2026
Merged

v3#16
lirbank merged 42 commits into
mainfrom
v3

Conversation

@lirbank

@lirbank lirbank commented Jun 2, 2026

Copy link
Copy Markdown
Member

Fixes #15
Fixes #11

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Bun test runner support alongside Vitest compatibility
    • Introduced zero-config setup module (neon-testing/setup)
    • Separated entry points for different test runners and core functionality
  • Documentation

    • Updated README with new setup instructions and API reference for both runners
    • Enhanced examples for Vitest and Bun integration
  • Chores

    • Updated minimum Node.js requirement to version 22
    • Reorganized package exports for improved modularity
    • Improved CI/CD workflows and test infrastructure

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@lirbank, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 43 minutes and 7 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

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.

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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34b6896e-229b-4034-be27-30b9422ad2ac

📥 Commits

Reviewing files that changed from the base of the PR and between 1733be8 and e26a830.

📒 Files selected for processing (1)
  • README.md
📝 Walkthrough

Walkthrough

This PR implements neon-testing v3.0: the Vitest-only core is refactored into a runner-agnostic makeNeonTestingCore with injected lifecycle hooks, new neon-testing/vitest and neon-testing/bun subpath entries wire the appropriate runner hooks, neon-testing/setup replaces the Vite plugin for cross-runner DATABASE_URL clearing, four lib helpers are extracted, and complete Bun and Vitest driver integration test suites are added.

Changes

neon-testing v3.0: Bun support, TS-native, runner-agnostic core

Layer / File(s) Summary
Package manifest, exports map, and build config
package.json, tsconfig.json, tsconfig.build.json, vitest.config.ts, bunfig.toml
package.json replaces dist-based exports with src-based subpath exports (./core, ./vitest, ./bun, ./utils, ./setup), removes vite peer, marks vitest optional, adds @types/bun, and rewrites scripts for Bun-native workflows. tsconfig.build.json is deleted; tsconfig.json gains moduleDetection: "force", erasableSyntaxOnly, and exactOptionalPropertyTypes. vitest.config.ts adds an ordered setupFiles pipeline and excludes tests-bun/**. bunfig.toml wires the Bun preload chain.
Extracted lib helpers: ssl, expires-in, with-retry, ws-error
src/lib/ssl.ts, src/lib/ssl.test.ts, src/lib/expires-in.ts, src/lib/expires-in.test.ts, src/lib/with-retry.ts, src/lib/with-retry.test.ts, src/lib/ws-error.ts, src/lib/ws-error.test.ts, src/lib/singleton.test.ts, src/lib/barrier.test.ts
Four helpers are extracted from the old monolithic core.ts: applySslMode, validateExpiresIn, withRetry (HTTP 423 exponential backoff), and neonWsErrorHandler. Each ships with a Vitest unit test suite; lazySingleton and createBarrier gain unit tests too.
Runner-agnostic core with injected lifecycle hooks
src/core.ts
NeonTestingHooks interface and MakeNeonTestingCoreOptions (requiring hooks) are introduced. The exported factory changes to makeNeonTestingCore; it registers setup/teardown via injected hooks, switches branch payload to conditional spreads, and uses delete process.env.DATABASE_URL in teardown. New MakeNeonTestingOptions and per-file NeonTestingOptions type aliases are exported.
Runner entry points, setup module, and Vite plugin removal
src/vitest.ts, src/bun.ts, src/setup.ts, src/utils.ts, src/vite-plugin.ts, src/vitest-setup.ts, src/browser-empty.ts
src/vitest.ts and src/bun.ts each define makeNeonTesting by injecting their runner's beforeAll/afterAll into makeNeonTestingCore. src/setup.ts provides the zero-config DATABASE_URL clearing preload. src/utils.ts drops the deprecated neonTesting Vite export. src/vite-plugin.ts, src/vitest-setup.ts, and src/browser-empty.ts are removed.
Test harnesses, preload/poison/smoke scaffolding (both runners)
tests-vitest/neon-testing.ts, tests-bun/neon-testing.ts, tests-vitest/poison-database-url.ts, tests-bun/poison-database-url.ts, tests-vitest/capture-database-url.ts, tests-bun/capture-database-url.ts, tests-vitest/preload.test.ts, tests-bun/preload.test.ts, tests-vitest/smoke.test.ts, tests-bun/smoke.test.ts
Each runner gets a harness module (neon-testing.ts) that re-exports test primitives and a configured neonTesting instance. Poison preloads set DATABASE_URL to an invalid value and mark a global flag; capture utilities snapshot the value after all preloads; preload tests verify the clearing behavior. Smoke tests confirm a branch is created and the connection string works.
Vitest driver integration test suites
tests-vitest/drivers/*
New suites for Neon HTTP, HTTP+Drizzle, TCP pg, TCP pg+Drizzle, TCP Postgres.js, TCP Postgres.js+Drizzle, WebSocket Neon, and WebSocket Neon+Drizzle. Each parameterizes pooler/direct endpoints and tests table creation, insert/select, non-isolation within a file, and transaction semantics. WebSocket tests call end() explicitly.
Bun driver integration test suites and explicit WS teardown
tests-bun/drivers/*
All Bun driver tests switch imports from vitest to the local ../neon-testing harness. WebSocket tests remove autoCloseWebSockets: true and replace it with explicit await end() calls. TCP endpoint parameterization changes to typed tuple form.
Examples, README, CI workflows, and dev config
examples/*, README.md, AGENTS.md, CLAUDE.md, .github/workflows/publish.yml, .github/workflows/test.yml, .vscode/settings.json
All examples update imports to neon-testing/vitest. README is rewritten for Bun/Vitest dual-runner setup, documenting neon-testing/setup, runner-specific entry points, and updated API reference. CI test.yml adds concurrency and upgrades to Node 24; publish.yml splits into check+publish jobs with tag/version consistency verification.

Sequence Diagram(s)

sequenceDiagram
  participant TestFile as Test file (Vitest or Bun)
  participant Entry as neon-testing/vitest or /bun
  participant Core as makeNeonTestingCore
  participant Setup as neon-testing/setup (preload)
  participant NeonAPI as Neon Branch API

  Setup->>Setup: delete process.env.DATABASE_URL
  TestFile->>Entry: import makeNeonTesting
  Entry->>Core: makeNeonTestingCore({ hooks: runner.beforeAll/afterAll, ... })
  Core-->>TestFile: neonTesting(overrides)
  TestFile->>Core: neonTesting() called at file scope
  Core->>Core: hooks.beforeAll(setup)
  Core->>NeonAPI: createProjectBranch(parentId, expiresAt)
  NeonAPI-->>Core: branch connection string
  Core->>Core: process.env.DATABASE_URL = connectionString
  TestFile->>TestFile: run tests using DATABASE_URL
  Core->>Core: hooks.afterAll(teardown)
  Core->>NeonAPI: deleteProjectBranch
  Core->>Core: delete process.env.DATABASE_URL
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • starmode-base/neon-testing#7: Directly connected — the validateExpiresIn function extracted into src/lib/expires-in.ts in this PR implements the expiring branches feature tracked in that PR.
  • starmode-base/neon-testing#12: Both PRs touch createBarrier — PR #12 added/exported it, and this PR moves it to src/lib/barrier and re-exports it from src/utils.ts while adding src/lib/barrier.test.ts.

Poem

🐰 A rabbit hops from Vite's old den,
Now Bun and Vitest both get zen —
Inject the hooks, let core stay clean,
neon-testing/bun joins the scene!
No prod DB writes, no silent fail,
The setup preload tells the tale. 🌿

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v3

@lirbank
lirbank marked this pull request as ready for review June 16, 2026 01:01

@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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests-bun/drivers/ws-neon-drizzle.test.ts (1)

65-132: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

shared root cause: websocket teardown is not exception-safe

both suites rely on explicit close for bun, but end() currently executes only on success paths. make teardown unconditional with try/finally in every test that opens a connection.

  • tests-bun/drivers/ws-neon-drizzle.test.ts#L65-L132: wrap each test body in try/finally and call await end() in finally.
  • tests-bun/drivers/ws-neon.test.ts#L41-L108: apply the same try/finally teardown pattern for all tests using makeDb(...).
🤖 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 `@tests-bun/drivers/ws-neon-drizzle.test.ts` around lines 65 - 132, The
websocket teardown via end() is not exception-safe because it only executes on
the success path. At tests-bun/drivers/ws-neon-drizzle.test.ts lines 65-132,
wrap the test body of each of the three tests ("create table", "tests are not
isolated within a test file", "interactive transactions are supported") in a
try/finally block, moving the await end() call from the end of the test body
into the finally block to ensure the connection is unconditionally closed. Apply
the same try/finally teardown pattern to all tests using makeDb(...) in
tests-bun/drivers/ws-neon.test.ts lines 41-108.
src/core.ts (1)

342-352: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

remove the uncaughtException listener after websocket cleanup

Line 348 installs a process-global listener but it is never removed. With multiple files using autoCloseWebSockets, handlers accumulate and can cause MaxListenersExceededWarning plus unintended swallowing beyond teardown.

proposed fix
     factoryOptions.hooks.afterAll(async () => {
       delete process.env.DATABASE_URL;

+      let wsErrorHandlerAttached = false;
+
       // Close all tracked Neon WebSocket connections before deleting the branch
       if (options.autoCloseWebSockets && neonSockets) {
         // Suppress Neon WebSocket "Connection terminated unexpectedly" error
         process.prependListener("uncaughtException", neonWsErrorHandler);
+        wsErrorHandlerAttached = true;

         // Close tracked Neon WebSocket connections before deleting the branch
         neonSockets.forEach((ws) => ws.close());
       }

-      if (options.deleteBranch !== false) {
-        await deleteBranch();
+      try {
+        if (options.deleteBranch !== false) {
+          await deleteBranch();
+        }
+      } finally {
+        if (wsErrorHandlerAttached) {
+          process.removeListener("uncaughtException", neonWsErrorHandler);
+        }
       }
     });
🤖 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 `@src/core.ts` around lines 342 - 352, The process.prependListener call for
"uncaughtException" with neonWsErrorHandler on line 348 is never removed,
causing the listener to accumulate across multiple test file executions when
autoCloseWebSockets is enabled, leading to MaxListenersExceededWarning and
unintended exception swallowing beyond the intended teardown phase. After the
neonSockets.forEach loop completes its cleanup, remove the listener by calling
process.removeListener with the same event name and handler function to ensure
it does not persist beyond the WebSocket closure and does not interfere with
subsequent test teardowns.
🤖 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 @.github/workflows/publish.yml:
- Line 24: Replace all floating version tags with immutable commit SHAs across
the workflow files to strengthen supply-chain integrity. In
.github/workflows/publish.yml, update line 24 to pin actions/checkout from `@v6`
to its full commit SHA, line 32 to pin actions/setup-node from `@v6` to its full
commit SHA, line 70 to pin actions/checkout from `@v6` to its full commit SHA, and
line 76 to pin actions/setup-node from `@v6` to its full commit SHA. In
.github/workflows/test.yml, update line 38 to pin actions/checkout from `@v6` to
its full commit SHA and line 49 to pin actions/setup-node from `@v6` to its full
commit SHA. Each uses statement should reference the complete commit SHA of the
action version instead of the semantic version tag.
- Around line 68-71: The checkout action in the publish job is persisting the
GITHUB_TOKEN to .git/config by default, which is a security risk since no
subsequent steps require git authentication. Add the `persist-credentials:
false` parameter to the `actions/checkout@v6` action in the publish job checkout
step to disable this credential persistence, matching the security pattern
already implemented in the check job.

In `@package.json`:
- Line 3: Update the version field in package.json from "2.7.0" to "3.0.0" to
reflect the breaking changes introduced in this PR. Since the PR contains
breaking changes to the public API and package surface, it requires a major
version bump according to semantic versioning conventions rather than a minor
version increment.

In `@tests-bun/smoke.test.ts`:
- Around line 9-13: The pool cleanup in the test case is not guaranteed to
execute if the query or assertion fails, leaving connections open for subsequent
tests. Wrap the pool.query() call and expect() assertion in a try block, and
move the pool.end() call to a finally block to ensure the pool is always
properly closed regardless of whether the query succeeds or the assertion
passes.

In `@tests-vitest/drivers/tcp-pg-drizzle.test.ts`:
- Around line 34-56: The connection teardown code is currently placed only in
the success path of each test function, causing resource leaks if any query or
assertion fails. Move the teardown code into a finally block to ensure it always
executes regardless of test outcome. In
tests-vitest/drivers/tcp-pg-drizzle.test.ts (lines 34-56), wrap await
pool.$client.end() in finally. In tests-vitest/drivers/tcp-pg.test.ts (lines
20-42), wrap await pool.end() in finally. In
tests-vitest/drivers/ws-neon-drizzle.test.ts (lines 65-87), wrap await end() in
finally. In tests-vitest/drivers/ws-neon.test.ts (lines 41-63), wrap await end()
in finally. This ensures proper cleanup even if test assertions or database
operations fail.

In `@tests-vitest/drivers/tcp-pg.test.ts`:
- Line 45: On line 45 of tests-vitest/drivers/tcp-pg.test.ts where the Pool is
instantiated, add a non-null assertion to DATABASE_URL (using the ! operator
after process.env.DATABASE_URL) to match the null guards already applied on
lines 21 and 66 in the same file. This prevents the pg Pool from falling back to
PostgreSQL environment variables when DATABASE_URL is undefined, which could
cause the integration test to connect to an unintended database.

In `@tests-vitest/smoke.test.ts`:
- Around line 9-13: The pool.end() call is only executed on the happy path when
all assertions pass, which means if the query or expect statements fail, the
connection remains open and leaks across the test process. Refactor the test by
wrapping the pool.query() call and the expect assertion in a try block, then
place the await pool.end() call in a finally block to ensure the database
connection is always closed whether the test succeeds or fails.

---

Outside diff comments:
In `@src/core.ts`:
- Around line 342-352: The process.prependListener call for "uncaughtException"
with neonWsErrorHandler on line 348 is never removed, causing the listener to
accumulate across multiple test file executions when autoCloseWebSockets is
enabled, leading to MaxListenersExceededWarning and unintended exception
swallowing beyond the intended teardown phase. After the neonSockets.forEach
loop completes its cleanup, remove the listener by calling
process.removeListener with the same event name and handler function to ensure
it does not persist beyond the WebSocket closure and does not interfere with
subsequent test teardowns.

In `@tests-bun/drivers/ws-neon-drizzle.test.ts`:
- Around line 65-132: The websocket teardown via end() is not exception-safe
because it only executes on the success path. At
tests-bun/drivers/ws-neon-drizzle.test.ts lines 65-132, wrap the test body of
each of the three tests ("create table", "tests are not isolated within a test
file", "interactive transactions are supported") in a try/finally block, moving
the await end() call from the end of the test body into the finally block to
ensure the connection is unconditionally closed. Apply the same try/finally
teardown pattern to all tests using makeDb(...) in
tests-bun/drivers/ws-neon.test.ts lines 41-108.
🪄 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: 85df39a9-86ea-4c3f-87b7-8eaa45eb9e0e

📥 Commits

Reviewing files that changed from the base of the PR and between 415f226 and 1733be8.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (62)
  • .github/workflows/publish.yml
  • .github/workflows/test.yml
  • .vscode/settings.json
  • AGENTS.md
  • CLAUDE.md
  • README.md
  • bunfig.toml
  • examples/connection-uri.test.ts
  • examples/minimal.test.ts
  • examples/neon-testing.ts
  • examples/options-validation.test.ts
  • examples/recommended.test.ts
  • package.json
  • src/browser-empty.ts
  • src/bun.ts
  • src/core.ts
  • src/lib/barrier.test.ts
  • src/lib/barrier.ts
  • src/lib/expires-in.test.ts
  • src/lib/expires-in.ts
  • src/lib/singleton.test.ts
  • src/lib/singleton.ts
  • src/lib/ssl.test.ts
  • src/lib/ssl.ts
  • src/lib/with-retry.test.ts
  • src/lib/with-retry.ts
  • src/lib/ws-error.test.ts
  • src/lib/ws-error.ts
  • src/setup.ts
  • src/utils.ts
  • src/vite-plugin.ts
  • src/vitest-setup.ts
  • src/vitest.ts
  • tests-bun/capture-database-url.ts
  • tests-bun/drivers/http-neon-drizzle.test.ts
  • tests-bun/drivers/http-neon.test.ts
  • tests-bun/drivers/tcp-pg-drizzle.test.ts
  • tests-bun/drivers/tcp-pg.test.ts
  • tests-bun/drivers/tcp-postgres-drizzle.test.ts
  • tests-bun/drivers/tcp-postgres.test.ts
  • tests-bun/drivers/ws-neon-drizzle.test.ts
  • tests-bun/drivers/ws-neon.test.ts
  • tests-bun/neon-testing.ts
  • tests-bun/poison-database-url.ts
  • tests-bun/preload.test.ts
  • tests-bun/smoke.test.ts
  • tests-vitest/capture-database-url.ts
  • tests-vitest/drivers/http-neon-drizzle.test.ts
  • tests-vitest/drivers/http-neon.test.ts
  • tests-vitest/drivers/tcp-pg-drizzle.test.ts
  • tests-vitest/drivers/tcp-pg.test.ts
  • tests-vitest/drivers/tcp-postgres-drizzle.test.ts
  • tests-vitest/drivers/tcp-postgres.test.ts
  • tests-vitest/drivers/ws-neon-drizzle.test.ts
  • tests-vitest/drivers/ws-neon.test.ts
  • tests-vitest/neon-testing.ts
  • tests-vitest/poison-database-url.ts
  • tests-vitest/preload.test.ts
  • tests-vitest/smoke.test.ts
  • tsconfig.build.json
  • tsconfig.json
  • vitest.config.ts
💤 Files with no reviewable changes (4)
  • src/vitest-setup.ts
  • src/vite-plugin.ts
  • src/browser-empty.ts
  • tsconfig.build.json

steps:
- name: Checkout
# https://github.com/actions/checkout
uses: actions/checkout@v6

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# verify mutable action refs (non-SHA) in workflow files
rg -nP '^\s*uses:\s*[^@]+@(?![0-9a-fA-F]{40}\b)[^\s]+' .github/workflows/*.yml

Repository: starmode-base/neon-testing

Length of output: 684


Pin all workflow action references to immutable commit SHAs

Using floating action tags (@v*) weakens supply-chain integrity. Pin the following actions to full commit SHAs instead:

  • .github/workflows/publish.yml#L24: actions/checkout@v6
  • .github/workflows/publish.yml#L32: actions/setup-node@v6
  • .github/workflows/publish.yml#L38: oven-sh/setup-bun@v2
  • .github/workflows/publish.yml#L70: actions/checkout@v6
  • .github/workflows/publish.yml#L76: actions/setup-node@v6
  • .github/workflows/publish.yml#L118: softprops/action-gh-release@v2
  • .github/workflows/test.yml#L38: actions/checkout@v6
  • .github/workflows/test.yml#L49: actions/setup-node@v6
  • .github/workflows/test.yml#L55: oven-sh/setup-bun@v2
🧰 Tools
🪛 zizmor (1.25.2)

[error] 24-24: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

📍 Affects 2 files
  • .github/workflows/publish.yml#L24-L24 (this comment)
  • .github/workflows/publish.yml#L32-L32
  • .github/workflows/publish.yml#L70-L70
  • .github/workflows/publish.yml#L76-L76
  • .github/workflows/test.yml#L38-L38
  • .github/workflows/test.yml#L49-L49
🤖 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 @.github/workflows/publish.yml at line 24, Replace all floating version tags
with immutable commit SHAs across the workflow files to strengthen supply-chain
integrity. In .github/workflows/publish.yml, update line 24 to pin
actions/checkout from `@v6` to its full commit SHA, line 32 to pin
actions/setup-node from `@v6` to its full commit SHA, line 70 to pin
actions/checkout from `@v6` to its full commit SHA, and line 76 to pin
actions/setup-node from `@v6` to its full commit SHA. In
.github/workflows/test.yml, update line 38 to pin actions/checkout from `@v6` to
its full commit SHA and line 49 to pin actions/setup-node from `@v6` to its full
commit SHA. Each uses statement should reference the complete commit SHA of the
action version instead of the semantic version tag.

Source: Linters/SAST tools

Comment on lines +68 to +71
- name: Checkout
# https://github.com/actions/checkout
uses: actions/checkout@v6

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/publish.yml

Repository: starmode-base/neon-testing

Length of output: 5142


Disable credential persistence in the publish checkout step

The publish job has write-capable permissions (contents: write). By default, checkout@v6 persists the GITHUB_TOKEN to .git/config, which is unnecessary since no subsequent steps require git authentication. This aligns with the security pattern already established in the check job (lines 25-28).

proposed fix
      - name: Checkout
        # https://github.com/actions/checkout
        uses: actions/checkout@v6
+       with:
+         persist-credentials: false
📝 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
- name: Checkout
# https://github.com/actions/checkout
uses: actions/checkout@v6
- name: Checkout
# https://github.com/actions/checkout
uses: actions/checkout@v6
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 68-70: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 70-70: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 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 @.github/workflows/publish.yml around lines 68 - 71, The checkout action in
the publish job is persisting the GITHUB_TOKEN to .git/config by default, which
is a security risk since no subsequent steps require git authentication. Add the
`persist-credentials: false` parameter to the `actions/checkout@v6` action in
the publish job checkout step to disable this credential persistence, matching
the security pattern already implemented in the check job.

Source: Linters/SAST tools

Comment thread package.json
@@ -1,12 +1,14 @@
{
"name": "neon-testing",
"version": "2.7.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

bump package version to a new major before publishing

Line 3 is still 2.7.0, but this PR introduces breaking public API/package-surface changes. Publishing under 2.x would violate semver expectations for consumers.

suggested update
-  "version": "2.7.0",
+  "version": "3.0.0",
📝 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
"version": "2.7.0",
"version": "3.0.0",
🤖 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 `@package.json` at line 3, Update the version field in package.json from
"2.7.0" to "3.0.0" to reflect the breaking changes introduced in this PR. Since
the PR contains breaking changes to the public API and package surface, it
requires a major version bump according to semantic versioning conventions
rather than a minor version increment.

Comment thread tests-bun/smoke.test.ts
Comment on lines +9 to +13
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const result = await pool.query("SELECT 1 AS value");
expect(result.rows[0].value).toBe(1);
await pool.end();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

ensure the pool is always closed on failures.

if the query or assertion fails, the current flow skips cleanup and can leave an open connection for subsequent tests.

suggested fix
 test("creates a branch and exposes a working DATABASE_URL", async () => {
   expect(process.env.DATABASE_URL).toBeDefined();

   const pool = new Pool({ connectionString: process.env.DATABASE_URL });
-  const result = await pool.query("SELECT 1 AS value");
-  expect(result.rows[0].value).toBe(1);
-  await pool.end();
+  try {
+    const result = await pool.query("SELECT 1 AS value");
+    expect(result.rows[0].value).toBe(1);
+  } finally {
+    await pool.end();
+  }
 });
📝 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
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const result = await pool.query("SELECT 1 AS value");
expect(result.rows[0].value).toBe(1);
await pool.end();
});
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
try {
const result = await pool.query("SELECT 1 AS value");
expect(result.rows[0].value).toBe(1);
} finally {
await pool.end();
}
});
🤖 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 `@tests-bun/smoke.test.ts` around lines 9 - 13, The pool cleanup in the test
case is not guaranteed to execute if the query or assertion fails, leaving
connections open for subsequent tests. Wrap the pool.query() call and expect()
assertion in a try block, and move the pool.end() call to a finally block to
ensure the pool is always properly closed regardless of whether the query
succeeds or the assertion passes.

Comment on lines +34 to +56
test("create table", async () => {
const pool = makeDb(process.env.DATABASE_URL!);

await pool.execute(`
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE
)
`);

const newUser = await pool.execute(`
INSERT INTO users (name)
VALUES ('Ellen Ripley')
RETURNING *
`);
expect(newUser.rows[0]).toStrictEqual({ id: 1, name: "Ellen Ripley" });

const users = await pool.execute(`SELECT * FROM users`);
expect(users.rows).toStrictEqual([{ id: 1, name: "Ellen Ripley" }]);

// 👎 Have to manually end the connection unless disabling `deleteBranch`
await pool.$client.end();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the test files exist and examine the tcp-pg-drizzle.test.ts file
fd -t f "tcp-pg-drizzle.test.ts" tests-vitest/

Repository: starmode-base/neon-testing

Length of output: 114


🏁 Script executed:

# Examine the tcp-pg-drizzle test file around the specified lines
wc -l tests-vitest/drivers/tcp-pg-drizzle.test.ts

Repository: starmode-base/neon-testing

Length of output: 118


🏁 Script executed:

# Read the tcp-pg-drizzle.test.ts file to verify the snippet
cat -n tests-vitest/drivers/tcp-pg-drizzle.test.ts

Repository: starmode-base/neon-testing

Length of output: 4060


🏁 Script executed:

# Check all 4 test files exist
fd -t f "tcp-pg-drizzle.test.ts|tcp-pg.test.ts|ws-neon-drizzle.test.ts|ws-neon.test.ts" tests-vitest/

Repository: starmode-base/neon-testing

Length of output: 232


🏁 Script executed:

# Check tcp-pg.test.ts
cat -n tests-vitest/drivers/tcp-pg.test.ts

Repository: starmode-base/neon-testing

Length of output: 3539


🏁 Script executed:

# Check ws-neon-drizzle.test.ts
cat -n tests-vitest/drivers/ws-neon-drizzle.test.ts

Repository: starmode-base/neon-testing

Length of output: 4896


🏁 Script executed:

# Check ws-neon.test.ts
cat -n tests-vitest/drivers/ws-neon.test.ts

Repository: starmode-base/neon-testing

Length of output: 4068


Move connection teardown into finally blocks to prevent resource leaks on test failures across all suites

All three test functions in each of these files place manual teardown (connection.end()) only at the success path, causing open connections to leak if any query or assertion fails:

  • tests-vitest/drivers/tcp-pg-drizzle.test.ts: Wrap await pool.$client.end() in finally (affects all tests in this file)
  • tests-vitest/drivers/tcp-pg.test.ts: Wrap await pool.end() in finally (affects all tests in this file)
  • tests-vitest/drivers/ws-neon-drizzle.test.ts: Wrap await end() in finally (affects all tests in this file)
  • tests-vitest/drivers/ws-neon.test.ts: Wrap await end() in finally (affects all tests in this file)
📍 Affects 4 files
  • tests-vitest/drivers/tcp-pg-drizzle.test.ts#L34-L56 (this comment)
  • tests-vitest/drivers/tcp-pg.test.ts#L20-L42
  • tests-vitest/drivers/ws-neon-drizzle.test.ts#L65-L87
  • tests-vitest/drivers/ws-neon.test.ts#L41-L63
🤖 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 `@tests-vitest/drivers/tcp-pg-drizzle.test.ts` around lines 34 - 56, The
connection teardown code is currently placed only in the success path of each
test function, causing resource leaks if any query or assertion fails. Move the
teardown code into a finally block to ensure it always executes regardless of
test outcome. In tests-vitest/drivers/tcp-pg-drizzle.test.ts (lines 34-56), wrap
await pool.$client.end() in finally. In tests-vitest/drivers/tcp-pg.test.ts
(lines 20-42), wrap await pool.end() in finally. In
tests-vitest/drivers/ws-neon-drizzle.test.ts (lines 65-87), wrap await end() in
finally. In tests-vitest/drivers/ws-neon.test.ts (lines 41-63), wrap await end()
in finally. This ensures proper cleanup even if test assertions or database
operations fail.

});

test("tests are not isolated within a test file", async () => {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n tests-vitest/drivers/tcp-pg.test.ts | head -100

Repository: starmode-base/neon-testing

Length of output: 3539


🌐 Web query:

pg nodejs pool undefined connectionString fallback behavior

💡 Result:

In node-postgres, the Pool constructor does not have a "fallback" behavior that automatically looks for a specific connection string when the value is undefined [1]. If you pass an undefined connectionString to the Pool constructor (e.g., new Pool({ connectionString: undefined })), the library does not treat it as a trigger to look for a specific environment variable like DATABASE_URL or POSTGRES_URL [1]. Instead, the Pool will attempt to connect using its default parameters (e.g., localhost, default port 5432, default user, etc.), which are often combined with relevant environment variables if they exist in the process environment [1]. Key points regarding this behavior: 1. No Automatic Lookup: The Pool does not internally look for a "default" environment variable if the connectionString property is explicitly passed as undefined [1]. It behaves as if no connection string was provided [1]. 2. Default Connection Parameters: When no specific connection configuration is provided, the driver relies on standard PostgreSQL environment variables (such as PGHOST, PGUSER, PGDATABASE, PGPASSWORD, and PGPORT) if they are set in the system environment [1]. 3. Explicit Configuration is Best: If you need to support fallback behavior (e.g., using a local development default if an environment variable is missing), you must implement this logic in your application code before initializing the Pool [1]: const pool = new Pool({ connectionString: process.env.DATABASE_URL || 'postgresql://localhost:5432/my_default_db', }); Note that certain third-party libraries (like @vercel/postgres) may provide wrapper functions that include custom error handling or automatic environment variable lookups, but these are not features of the core node-postgres (pg) library itself [2].

Citations:


🌐 Web query:

node-postgres Pool constructor connectionString undefined environment variables

💡 Result:

In node-postgres, the Pool constructor and environment variables interact to determine connection settings in a specific priority order [1][2]. When you instantiate a Pool without arguments (e.g., new Pool), node-postgres automatically looks for and uses standard PostgreSQL environment variables to configure the connection [1][2]. These include variables such as PGUSER, PGPASSWORD, PGHOST, PGPORT, and PGDATABASE [1][2]. If you provide a configuration object to the Pool constructor, those values take precedence over environment variables [1][2]. If you specifically pass a connectionString property within that object, it is parsed to establish the connection details [1][3]. Regarding your query about undefined values: 1. Environment Variable Fallbacks: If you initialize the Pool without specific configuration (new Pool), the library defaults to specific environment variables [1][3]. If those environment variables are also undefined in your environment, the library falls back to system defaults (e.g., PGHOST defaults to localhost, PGPORT to 5432, and PGUSER/PGDATABASE to the current OS user) [1][3]. 2. Constructor Configuration: If you pass an object to the Pool constructor, explicit values provided in that object will override any environment variables [1]. If a property in your configuration object is explicitly set to undefined (e.g., { host: undefined }), it will generally be treated as if that property was omitted, allowing the library to fall back to the relevant environment variable or system default for that specific setting [1][2]. 3. Connection Strings: If you use the connectionString property, it must be a valid string [1][3]. If you pass an undefined value as the connectionString, the pool may fail to initialize or behave unexpectedly because it expects a valid URI format [1][3]. If you are working in an environment (like some cloud platforms) where the connection string might be missing or undefined, you should check for its existence before passing it to the Pool constructor [1][2]. If you need to handle connection configurations dynamically, it is common practice to parse the connection string separately or check for the existence of your environment variables before instantiating the pool to ensure you are passing valid configuration objects [2][4].

Citations:


Add null guard for DATABASE_URL to prevent unexpected database connections

Line 45 passes process.env.DATABASE_URL without a non-null assertion. When undefined, the pg Pool falls back to PostgreSQL environment variables (like PGHOST, PGDATABASE, etc.), potentially connecting to an unintended database—critical in integration tests. Lines 21 and 66 in this same file already guard with !, making line 45 inconsistent and risky.

proposed fix
-    const pool = new Pool({ connectionString: process.env.DATABASE_URL });
+    const databaseUrl = process.env.DATABASE_URL;
+    if (!databaseUrl) throw new Error("DATABASE_URL is not set");
+    const pool = new Pool({ connectionString: databaseUrl });
📝 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
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) throw new Error("DATABASE_URL is not set");
const pool = new Pool({ connectionString: databaseUrl });
🤖 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 `@tests-vitest/drivers/tcp-pg.test.ts` at line 45, On line 45 of
tests-vitest/drivers/tcp-pg.test.ts where the Pool is instantiated, add a
non-null assertion to DATABASE_URL (using the ! operator after
process.env.DATABASE_URL) to match the null guards already applied on lines 21
and 66 in the same file. This prevents the pg Pool from falling back to
PostgreSQL environment variables when DATABASE_URL is undefined, which could
cause the integration test to connect to an unintended database.

Comment on lines +9 to +13
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const result = await pool.query("SELECT 1 AS value");
expect(result.rows[0].value).toBe(1);
await pool.end();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

ensure the pool is always closed on failure paths

pool.end() currently runs only on the happy path. Wrap query/assertions in try/finally so failures do not leak open connections across the test process.

suggested patch
 test("creates a branch and exposes a working DATABASE_URL", async () => {
   expect(process.env.DATABASE_URL).toBeDefined();

   const pool = new Pool({ connectionString: process.env.DATABASE_URL });
-  const result = await pool.query("SELECT 1 AS value");
-  expect(result.rows[0].value).toBe(1);
-  await pool.end();
+  try {
+    const result = await pool.query("SELECT 1 AS value");
+    expect(result.rows[0].value).toBe(1);
+  } finally {
+    await pool.end();
+  }
 });
🤖 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 `@tests-vitest/smoke.test.ts` around lines 9 - 13, The pool.end() call is only
executed on the happy path when all assertions pass, which means if the query or
expect statements fail, the connection remains open and leaks across the test
process. Refactor the test by wrapping the pool.query() call and the expect
assertion in a try block, then place the await pool.end() call in a finally
block to ensure the database connection is always closed whether the test
succeeds or fails.

@lirbank
lirbank merged commit bf890b6 into main Jun 16, 2026
2 checks passed
@lirbank
lirbank deleted the v3 branch June 16, 2026 01:31
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.0 — Bun support, stronger prod-DB safeguard, TS-native Bun test runner support

1 participant