Skip to content

Commit 13ef543

Browse files
authored
test: add deeper smoke tests for binary and npm bundle (#1013)
## Problem Our current smoke tests only run `--help` and `--version`, which exercise none of the lazy-loaded code paths (SQLite, telemetry, auth DB, Ink sidecar). This allowed two critical bugs to ship: 1. `_require("../telemetry.js")` in the CJS bundle — `--help` never triggers the lazy DB init 2. `with { type: "file" }` crashing in tsx dev mode — `--help` never loads the Ink sidecar ## Changes ### CI Build Smoke Tests (`.github/workflows/ci.yml`) **`build-binary` job**: Added a "Smoke test (deep)" step that runs `auth status` (unauthenticated) on the compiled binary. Exercises SQLite init, schema migrations, telemetry lazy import, and CJS require chain. Asserts exit code 10 (`AUTH_NOT_AUTHENTICATED`) — any other code (1=crash, 127=missing) fails the step. **`build-npm` job**: Same test via `node dist/bin.cjs auth status` on both Node 22 and Node 24. ### E2E Bundle Tests (`test/e2e/bundle.test.ts`) Three new tests: 1. **`auth status`** — exercises SQLite + telemetry + auth DB via npm bundle. Asserts exit 10, "not authenticated", no module resolution errors. 2. **`cli defaults`** — exercises SQLite metadata KV store without requiring auth. Asserts exit 0. 3. **Ink sidecar import** — directly imports `dist/ink-app.js` and verifies it exports `mountApp` as a function. Catches sidecar bundling bugs. ### Why `auth status`? - Sets `auth: false` on the command, so Stricli's auth guard is skipped — the command itself checks auth state - Exercises: SQLite init, DB schema + migrations, `getAuthConfig()`, `getUserInfo()`, `getDefaultOrganization/Project()`, `getDbPath()`, telemetry lazy import, error formatting pipeline - When unauthenticated: exits with code 10 deterministically, no network calls, ~200-500ms ### Why a direct sidecar import test? `init --dry-run` is not viable for smoke testing because: (a) `init` requires auth, (b) `--dry-run` forces `LoggingUI` not `InkUI`, (c) non-TTY CI forces `LoggingUI` regardless. The direct import test catches the exact class of sidecar bundling bugs without needing auth, a TTY, or a mock server. ## Timing Impact ~1-2 seconds added per build job (well under the 2-3s budget from the issue). Closes #1010
1 parent e7dd817 commit 13ef543

2 files changed

Lines changed: 134 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,32 @@ jobs:
309309
else
310310
./dist-bin/sentry-${{ matrix.target }} --help
311311
fi
312+
- name: Smoke test (deep — SQLite, telemetry, auth DB)
313+
if: matrix.can-test
314+
shell: bash
315+
env:
316+
SENTRY_AUTH_TOKEN: ""
317+
SENTRY_TOKEN: ""
318+
run: |
319+
if [[ "${{ matrix.target }}" == "windows-x64" ]]; then
320+
BIN=./dist-bin/sentry-windows-x64.exe
321+
else
322+
BIN=./dist-bin/sentry-${{ matrix.target }}
323+
fi
324+
# auth status without a token exercises SQLite init, schema
325+
# migrations, telemetry lazy import, and the CJS require chain.
326+
# Expected: exit 10 (AUTH_NOT_AUTHENTICATED), NOT a crash/syntax error.
327+
OUTPUT=$($BIN auth status 2>&1) && EXIT_CODE=$? || EXIT_CODE=$?
328+
if [[ $EXIT_CODE -ne 10 ]]; then
329+
echo "::error::Expected exit code 10 (not authenticated), got $EXIT_CODE"
330+
echo "$OUTPUT"
331+
exit 1
332+
fi
333+
if ! echo "$OUTPUT" | grep -qi "not authenticated"; then
334+
echo "::error::Expected 'not authenticated' in output, got:"
335+
echo "$OUTPUT"
336+
exit 1
337+
fi
312338
- name: Upload binary artifact
313339
uses: actions/upload-artifact@v7
314340
with:
@@ -710,6 +736,26 @@ jobs:
710736
run: pnpm run bundle
711737
- name: Smoke test (Node.js)
712738
run: node dist/bin.cjs --help
739+
- name: Smoke test (Node.js — deep)
740+
shell: bash
741+
env:
742+
SENTRY_AUTH_TOKEN: ""
743+
SENTRY_TOKEN: ""
744+
run: |
745+
# auth status without a token exercises SQLite init, schema
746+
# migrations, telemetry lazy import, and the CJS require chain.
747+
# Expected: exit 10 (AUTH_NOT_AUTHENTICATED), NOT a crash/syntax error.
748+
OUTPUT=$(node dist/bin.cjs auth status 2>&1) && EXIT_CODE=$? || EXIT_CODE=$?
749+
if [[ $EXIT_CODE -ne 10 ]]; then
750+
echo "::error::Expected exit code 10 (not authenticated), got $EXIT_CODE"
751+
echo "$OUTPUT"
752+
exit 1
753+
fi
754+
if ! echo "$OUTPUT" | grep -qi "not authenticated"; then
755+
echo "::error::Expected 'not authenticated' in output, got:"
756+
echo "$OUTPUT"
757+
exit 1
758+
fi
713759
- run: npm pack
714760
- name: Upload artifact
715761
if: matrix.node == '22'

test/e2e/bundle.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { spawn } from "node:child_process";
99
import { existsSync, rmSync } from "node:fs";
1010
import { readFile } from "node:fs/promises";
1111
import { join } from "node:path";
12+
import { pathToFileURL } from "node:url";
1213
import { afterAll, beforeAll, describe, expect, test } from "vitest";
1314

1415
function noop(): void {
@@ -45,6 +46,7 @@ async function spawnCollect(
4546

4647
const ROOT_DIR = join(import.meta.dirname, "../..");
4748
const BUNDLE_PATH = join(ROOT_DIR, "dist/bin.cjs");
49+
const INK_APP_PATH = join(ROOT_DIR, "dist/ink-app.js");
4850

4951
describe("npm bundle", () => {
5052
beforeAll(async () => {
@@ -149,4 +151,90 @@ describe("npm bundle", () => {
149151
const output = stdout + stderr;
150152
expect(output.length).toBeGreaterThan(0);
151153
}, 15_000); // Allow up to 15s for cold Node.js JIT startup on slow CI runners
154+
155+
test("bundle exercises SQLite and auth DB (auth status)", async () => {
156+
// Run `auth status` without a token — exercises SQLite initialization,
157+
// schema migrations, auth DB reads, and telemetry lazy import.
158+
// This catches lazy-import and require-resolution bugs that --version misses.
159+
const { stdout, stderr, exitCode } = await spawnCollect(
160+
"node",
161+
[BUNDLE_PATH, "auth", "status"],
162+
{
163+
cwd: ROOT_DIR,
164+
env: {
165+
...process.env,
166+
// Do NOT set SENTRY_CLI_NO_TELEMETRY — we want to exercise telemetry init
167+
SENTRY_AUTH_TOKEN: "",
168+
SENTRY_TOKEN: "",
169+
},
170+
}
171+
);
172+
173+
const output = stdout + stderr;
174+
175+
// Must exit 10 (AUTH_NOT_AUTHENTICATED), not crash
176+
expect(exitCode).toBe(10);
177+
expect(output.toLowerCase()).toContain("not authenticated");
178+
179+
// Must not contain Node.js module resolution errors
180+
expect(output).not.toContain("Cannot find module");
181+
expect(output).not.toContain("MODULE_NOT_FOUND");
182+
expect(output).not.toContain("ERR_MODULE_NOT_FOUND");
183+
}, 15_000);
184+
185+
test("bundle exercises SQLite with cli defaults", async () => {
186+
// `cli defaults` (no args) reads all defaults from SQLite — exercises
187+
// DB init and the metadata KV store without requiring auth.
188+
const { stdout, stderr, exitCode } = await spawnCollect(
189+
"node",
190+
[BUNDLE_PATH, "cli", "defaults"],
191+
{
192+
cwd: ROOT_DIR,
193+
env: {
194+
...process.env,
195+
SENTRY_AUTH_TOKEN: "",
196+
SENTRY_TOKEN: "",
197+
},
198+
}
199+
);
200+
201+
const output = stdout + stderr;
202+
203+
// Should succeed — cli defaults with no args just shows current state
204+
expect(exitCode).toBe(0);
205+
206+
// Must not contain module resolution errors
207+
expect(output).not.toContain("Cannot find module");
208+
expect(output).not.toContain("MODULE_NOT_FOUND");
209+
}, 15_000);
210+
211+
test("Ink sidecar can be imported and exports mountApp", async () => {
212+
// The Ink sidecar (dist/ink-app.js) is a pre-bundled self-contained ESM
213+
// module that ships with the npm package. Verify it exists, can be imported
214+
// by Node, and exports mountApp as a function. This catches sidecar
215+
// bundling/resolution bugs — the exact class of bug where `with { type: "file" }`
216+
// crashed in tsx dev mode.
217+
//
218+
// Run in a subprocess to avoid polluting the vitest process with
219+
// React/Ink globals from the sidecar's bundled dependencies.
220+
expect(existsSync(INK_APP_PATH)).toBe(true);
221+
222+
const { stdout, stderr, exitCode } = await spawnCollect("node", [
223+
"--input-type=module",
224+
"-e",
225+
`import { mountApp } from ${JSON.stringify(pathToFileURL(INK_APP_PATH).href)};\n` +
226+
'if (typeof mountApp !== "function") {\n' +
227+
' process.stderr.write("mountApp is " + typeof mountApp + ", expected function");\n' +
228+
" process.exit(1);\n" +
229+
"}",
230+
]);
231+
232+
if (exitCode !== 0) {
233+
const output = stdout + stderr;
234+
throw new Error(
235+
`Ink sidecar import failed (exit ${exitCode}): ${output}`
236+
);
237+
}
238+
expect(exitCode).toBe(0);
239+
}, 15_000);
152240
});

0 commit comments

Comments
 (0)