Skip to content

Commit 7713a6e

Browse files
committed
refactor: migrate test runner from bun:test to vitest (WIP)
Phase 3 of Bun → Node.js migration. Migrates 327 test files from bun:test to vitest. Mechanical changes: - bun:test imports → vitest imports (327 files) - mock → vi, spyOn → vi.spyOn, mock.module → vi.mock (52 files) - Bun.spawn/spawnSync → node:child_process in test files (12 files) - Bun.file/write/sleep/gzip/etc → Node.js equivalents in tests (40 files) - Bun.serve → node:http createServer in test mock server - import.meta.dir → import.meta.dirname (16 files) - BUN_TEST_WORKER_ID → VITEST_POOL_ID - Added vitest.config.ts with setupFiles, pool: forks, 15s timeout - Added custom toStartWith/toEndWith matchers in preload.ts - Removed bun:sqlite fallback from sqlite.ts - Updated package.json test scripts to use vitest Known remaining issues (WIP): - ESM spy limitations (Cannot spy on ESM namespace exports) - Some vi import gaps in edge cases - Test API signature changes (vitest 4) - self is not defined in model-based tests
1 parent 2e4435e commit 7713a6e

342 files changed

Lines changed: 4040 additions & 2425 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.lore.md

Lines changed: 20 additions & 62 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"typescript": "^5",
4949
"ultracite": "6.3.10",
5050
"uuidv7": "^1.1.0",
51+
"vitest": "^4.1.7",
5152
"wrap-ansi": "^10.0.0",
5253
"zod": "^3.24.0"
5354
},
@@ -93,10 +94,10 @@
9394
"lint": "biome check --no-errors-on-unmatched --max-diagnostics=none ./",
9495
"lint:fix": "biome check --write --no-errors-on-unmatched --max-diagnostics=none ./",
9596
"test": "bun run test:unit",
96-
"test:unit": "bun run generate:docs && bun run generate:sdk && bun test --timeout 15000 --isolate --parallel test/lib test/commands test/types --coverage --coverage-reporter=lcov",
97-
"test:changed": "bun run generate:docs && bun run generate:sdk && bun test --timeout 15000 --isolate --changed",
98-
"test:e2e": "bun run generate:docs && bun run generate:sdk && bun test --timeout 15000 test/e2e",
99-
"test:init-eval": "bun test test/init-eval --timeout 600000 --concurrency 6",
97+
"test:unit": "bun run generate:docs && bun run generate:sdk && vitest run test/lib test/commands test/types --coverage",
98+
"test:changed": "bun run generate:docs && bun run generate:sdk && vitest run --changed",
99+
"test:e2e": "bun run generate:docs && bun run generate:sdk && vitest run test/e2e",
100+
"test:init-eval": "vitest run test/init-eval --testTimeout 600000",
100101
"generate:parser": "bun run script/generate-parser.ts",
101102
"generate:sdk": "bun run script/generate-sdk.ts",
102103
"generate:skill": "bun run script/generate-skill.ts",

pnpm-lock.yaml

Lines changed: 678 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/db/sqlite.ts

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
* codebase. It provides a `.query(sql).get()` / `.all()` / `.run()`
66
* interface and a manual `transaction()` wrapper.
77
*
8-
* Uses `node:sqlite` (Node 22+) as the backing implementation. Falls back
9-
* to `bun:sqlite` when `node:sqlite` is unavailable (Bun runtime) — this
10-
* fallback will be removed once the test runner migrates off Bun.
8+
* Uses `node:sqlite` (Node 22+) as the backing implementation.
119
*/
1210

1311
import { logger } from "../logger.js";
@@ -58,29 +56,8 @@ function wrapStatement(stmt: any): StatementWrapper {
5856
}) as StatementWrapper;
5957
}
6058

61-
/**
62-
* Resolve the underlying SQLite database constructor.
63-
*
64-
* Prefers `node:sqlite` (Node 22+). Falls back to `bun:sqlite` when
65-
* `node:sqlite` is unavailable (Bun runtime). The fallback will be
66-
* removed once the test runner migrates off Bun.
67-
*/
68-
function getSqliteConstructor(): new (
69-
path: string
70-
) => {
71-
exec(sql: string): void;
72-
close(): void;
73-
} {
74-
try {
75-
return require("node:sqlite").DatabaseSync;
76-
} catch (error) {
77-
log.debug("node:sqlite unavailable, falling back to bun:sqlite", error);
78-
return require("bun:sqlite").Database;
79-
}
80-
}
81-
82-
// biome-ignore lint/suspicious/noExplicitAny: resolved dynamically
83-
const SqliteImpl: any = getSqliteConstructor();
59+
// biome-ignore lint/suspicious/noExplicitAny: node:sqlite types loaded lazily
60+
const { DatabaseSync: SqliteImpl } = require("node:sqlite") as any;
8461

8562
/**
8663
* SQLite database wrapper.

test/commands/api.property.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
* that are difficult to exhaustively test with example-based tests.
66
*/
77

8-
import { describe, expect, test } from "bun:test";
98
import {
109
array,
1110
asyncProperty,
@@ -21,6 +20,7 @@ import {
2120
tuple,
2221
uniqueArray,
2322
} from "fast-check";
23+
import { describe, expect, test } from "vitest";
2424
import {
2525
buildFromFields,
2626
extractJsonBody,

test/commands/api.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
* Tests for parsing functions in the api command.
66
*/
77

8-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
8+
import { writeFile } from "node:fs/promises";
99
import { Readable } from "node:stream";
10+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
1011
import {
1112
buildBodyFromFields,
1213
buildBodyFromInput,
@@ -1010,7 +1011,7 @@ describe("buildBodyFromInput", () => {
10101011
);
10111012
const testDir = await createTestConfigDir("test-api-file-");
10121013
const tempFile = `${testDir}/test-input.json`;
1013-
await Bun.write(tempFile, JSON.stringify({ key: "value" }));
1014+
await writeFile(tempFile, JSON.stringify({ key: "value" }));
10141015

10151016
try {
10161017
const mockStdin = createMockStdin("");
@@ -1028,7 +1029,7 @@ describe("buildBodyFromInput", () => {
10281029
);
10291030
const testDir = await createTestConfigDir("test-api-file-");
10301031
const tempFile = `${testDir}/test-input.txt`;
1031-
await Bun.write(tempFile, "plain text from file");
1032+
await writeFile(tempFile, "plain text from file");
10321033

10331034
try {
10341035
const mockStdin = createMockStdin("");

test/commands/auth/login.test.ts

Lines changed: 90 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* db/user, and interactive-login to cover all branches without real HTTP
77
* calls or database access.
88
*
9-
* The interactive TTY prompt tests use mock.module() at the top of this file
9+
* The interactive TTY prompt tests use vi.mock() at the top of this file
1010
* to stub node:tty (so isatty(0) returns true) and the logger module (so
1111
* `.prompt()` is controllable).
1212
*/
@@ -16,56 +16,63 @@ import {
1616
beforeEach,
1717
describe,
1818
expect,
19-
mock,
20-
spyOn,
2119
test,
22-
} from "bun:test";
20+
vi,
21+
} from "vitest";
2322

2423
// Mock isatty to simulate interactive terminal for the re-auth prompt path.
2524
// Bun's ESM wrapper for CJS built-ins exposes `default` + `ReadStream` +
2625
// `WriteStream` — all must be present.
27-
const mockIsatty = mock(() => false);
28-
class FakeReadStream {}
29-
class FakeWriteStream {}
30-
const ttyExports = {
31-
isatty: mockIsatty,
32-
ReadStream: FakeReadStream,
33-
WriteStream: FakeWriteStream,
34-
};
35-
mock.module("node:tty", () => ({
26+
const { mockIsatty, FakeReadStream, FakeWriteStream, ttyExports, noop, mockPrompt, fakeLog } = vi.hoisted(() => {
27+
const mockIsatty = vi.fn(() => false);
28+
class FakeReadStream {}
29+
class FakeWriteStream {}
30+
const ttyExports = {
31+
isatty: mockIsatty,
32+
ReadStream: FakeReadStream,
33+
WriteStream: FakeWriteStream,
34+
};
35+
36+
/** No-op placeholder for unused logger methods. */
37+
function noop() {
38+
// intentional no-op
39+
}
40+
41+
// Mock the logger module to intercept the .prompt() call made by the
42+
// module-scoped `log = logger.withTag("auth.login")` in login.ts.
43+
const mockPrompt = vi.fn(
44+
(): Promise<boolean | symbol> => Promise.resolve(true)
45+
);
46+
const fakeLog: {
47+
prompt: typeof mockPrompt;
48+
info: ReturnType<typeof vi.fn>;
49+
warn: ReturnType<typeof vi.fn>;
50+
error: ReturnType<typeof vi.fn>;
51+
debug: ReturnType<typeof vi.fn>;
52+
success: ReturnType<typeof vi.fn>;
53+
withTag: () => typeof fakeLog;
54+
} = {
55+
prompt: mockPrompt,
56+
info: vi.fn(noop),
57+
warn: vi.fn(noop),
58+
error: vi.fn(noop),
59+
debug: vi.fn(noop),
60+
success: vi.fn(noop),
61+
withTag: () => fakeLog,
62+
};
63+
64+
return { mockIsatty, FakeReadStream, FakeWriteStream, ttyExports, noop, mockPrompt, fakeLog };
65+
});
66+
67+
vi.mock("node:tty", () => ({
3668
...ttyExports,
3769
default: ttyExports,
3870
}));
3971

40-
/** No-op placeholder for unused logger methods. */
41-
function noop() {
42-
// intentional no-op
43-
}
44-
45-
// Mock the logger module to intercept the .prompt() call made by the
46-
// module-scoped `log = logger.withTag("auth.login")` in login.ts.
47-
const mockPrompt = mock((): Promise<boolean | symbol> => Promise.resolve(true));
48-
const fakeLog: {
49-
prompt: typeof mockPrompt;
50-
info: ReturnType<typeof mock>;
51-
warn: ReturnType<typeof mock>;
52-
error: ReturnType<typeof mock>;
53-
debug: ReturnType<typeof mock>;
54-
success: ReturnType<typeof mock>;
55-
withTag: () => typeof fakeLog;
56-
} = {
57-
prompt: mockPrompt,
58-
info: mock(noop),
59-
warn: mock(noop),
60-
error: mock(noop),
61-
debug: mock(noop),
62-
success: mock(noop),
63-
withTag: () => fakeLog,
64-
};
65-
mock.module("../../../src/lib/logger.js", () => ({
72+
vi.mock("../../../src/lib/logger.js", () => ({
6673
logger: fakeLog,
67-
setLogLevel: mock(noop),
68-
attachSentryReporter: mock(noop),
74+
setLogLevel: vi.fn(noop),
75+
attachSentryReporter: vi.fn(noop),
6976
LOG_LEVEL_NAMES: ["error", "warn", "log", "info", "debug", "trace"],
7077
LOG_LEVEL_ENV_VAR: "SENTRY_LOG_LEVEL",
7178
parseLogLevel: (name: string) => {
@@ -76,18 +83,34 @@ mock.module("../../../src/lib/logger.js", () => ({
7683
getEnvLogLevel: () => null,
7784
}));
7885

79-
// Dynamic import: must run AFTER mock.module() so login.ts picks up fakeLog.
86+
// Dynamic import: must run AFTER vi.mock() so login.ts picks up fakeLog.
8087
const { loginCommand, rcTokenHint } = await import(
8188
"../../../src/commands/auth/login.js"
8289
);
8390

91+
vi.mock("../../../src/lib/api-client.js", async (importOriginal) => {
92+
const actual = await importOriginal<typeof import("../../../src/lib/api-client.js")>();
93+
return Object.fromEntries(Object.entries(actual).map(([k, v]) => [k, typeof v === "function" ? vi.fn(v) : v]));
94+
});
8495
// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
8596
import * as apiClient from "../../../src/lib/api-client.js";
97+
vi.mock("../../../src/lib/db/auth.js", async (importOriginal) => {
98+
const actual = await importOriginal<typeof import("../../../src/lib/db/auth.js")>();
99+
return Object.fromEntries(Object.entries(actual).map(([k, v]) => [k, typeof v === "function" ? vi.fn(v) : v]));
100+
});
86101
// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
87102
import * as dbAuth from "../../../src/lib/db/auth.js";
103+
vi.mock("../../../src/lib/db/user.js", async (importOriginal) => {
104+
const actual = await importOriginal<typeof import("../../../src/lib/db/user.js")>();
105+
return Object.fromEntries(Object.entries(actual).map(([k, v]) => [k, typeof v === "function" ? vi.fn(v) : v]));
106+
});
88107
// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
89108
import * as dbUser from "../../../src/lib/db/user.js";
90109
import { AuthError } from "../../../src/lib/errors.js";
110+
vi.mock("../../../src/lib/interactive-login.js", async (importOriginal) => {
111+
const actual = await importOriginal<typeof import("../../../src/lib/interactive-login.js")>();
112+
return Object.fromEntries(Object.entries(actual).map(([k, v]) => [k, typeof v === "function" ? vi.fn(v) : v]));
113+
});
91114
// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
92115
import * as interactiveLogin from "../../../src/lib/interactive-login.js";
93116
import type { SentryCliRcConfig } from "../../../src/lib/sentryclirc.js";
@@ -121,12 +144,12 @@ function createContext() {
121144
const stdoutChunks: string[] = [];
122145
const context = {
123146
stdout: {
124-
write: mock((s: string) => {
147+
write: vi.fn((s: string) => {
125148
stdoutChunks.push(s);
126149
}),
127150
},
128151
stderr: {
129-
write: mock((_s: string) => {
152+
write: vi.fn((_s: string) => {
130153
// unused — diagnostics go through logger
131154
}),
132155
},
@@ -165,19 +188,19 @@ describe("loginCommand.func --token path", () => {
165188
let func: LoginFunc;
166189

167190
beforeEach(async () => {
168-
isAuthenticatedSpy = spyOn(dbAuth, "isAuthenticated");
169-
isEnvTokenActiveSpy = spyOn(dbAuth, "isEnvTokenActive");
170-
setAuthTokenSpy = spyOn(dbAuth, "setAuthToken");
171-
getUserRegionsSpy = spyOn(apiClient, "getUserRegions");
172-
clearAuthSpy = spyOn(dbAuth, "clearAuth");
173-
getCurrentUserSpy = spyOn(apiClient, "getCurrentUser");
174-
setUserInfoSpy = spyOn(dbUser, "setUserInfo");
175-
runInteractiveLoginSpy = spyOn(interactiveLogin, "runInteractiveLogin");
176-
hasStoredAuthCredentialsSpy = spyOn(dbAuth, "hasStoredAuthCredentials");
191+
isAuthenticatedSpy = vi.spyOn(dbAuth, "isAuthenticated");
192+
isEnvTokenActiveSpy = vi.spyOn(dbAuth, "isEnvTokenActive");
193+
setAuthTokenSpy = vi.spyOn(dbAuth, "setAuthToken");
194+
getUserRegionsSpy = vi.spyOn(apiClient, "getUserRegions");
195+
clearAuthSpy = vi.spyOn(dbAuth, "clearAuth");
196+
getCurrentUserSpy = vi.spyOn(apiClient, "getCurrentUser");
197+
setUserInfoSpy = vi.spyOn(dbUser, "setUserInfo");
198+
runInteractiveLoginSpy = vi.spyOn(interactiveLogin, "runInteractiveLogin");
199+
hasStoredAuthCredentialsSpy = vi.spyOn(dbAuth, "hasStoredAuthCredentials");
177200
// Prevent warmOrgCache() fire-and-forget from hitting real fetch.
178201
// After successful login, warmOrgCache() calls listOrganizationsUncached()
179202
// which triggers API calls that leak as "unexpected fetch" warnings.
180-
listOrgsUncachedSpy = spyOn(apiClient, "listOrganizationsUncached");
203+
listOrgsUncachedSpy = vi.spyOn(apiClient, "listOrganizationsUncached");
181204
listOrgsUncachedSpy.mockResolvedValue([]);
182205
isEnvTokenActiveSpy.mockReturnValue(false);
183206
hasStoredAuthCredentialsSpy.mockReturnValue(false);
@@ -429,7 +452,7 @@ describe("loginCommand.func --token path", () => {
429452
/**
430453
* Tests for the interactive TTY re-authentication prompt.
431454
*
432-
* Uses the module-level `mock.module()` on node:tty (so `isatty(0)` returns
455+
* Uses the module-level `vi.mock()` on node:tty (so `isatty(0)` returns
433456
* true) and the logger (so `.prompt()` is controllable).
434457
*/
435458
describe("login re-authentication interactive prompt", () => {
@@ -443,20 +466,20 @@ describe("login re-authentication interactive prompt", () => {
443466

444467
function createPromptContext() {
445468
return {
446-
stdout: { write: mock(() => true) },
447-
stderr: { write: mock(() => true) },
469+
stdout: { write: vi.fn(() => true) },
470+
stderr: { write: vi.fn(() => true) },
448471
cwd: "/tmp",
449472
};
450473
}
451474

452475
beforeEach(async () => {
453-
isAuthenticatedSpy = spyOn(dbAuth, "isAuthenticated");
454-
isEnvTokenActiveSpy = spyOn(dbAuth, "isEnvTokenActive");
455-
clearAuthSpy = spyOn(dbAuth, "clearAuth");
456-
runInteractiveLoginSpy = spyOn(interactiveLogin, "runInteractiveLogin");
457-
getUserInfoSpy = spyOn(dbUser, "getUserInfo");
476+
isAuthenticatedSpy = vi.spyOn(dbAuth, "isAuthenticated");
477+
isEnvTokenActiveSpy = vi.spyOn(dbAuth, "isEnvTokenActive");
478+
clearAuthSpy = vi.spyOn(dbAuth, "clearAuth");
479+
runInteractiveLoginSpy = vi.spyOn(interactiveLogin, "runInteractiveLogin");
480+
getUserInfoSpy = vi.spyOn(dbUser, "getUserInfo");
458481
// Prevent warmOrgCache() fire-and-forget from hitting real fetch.
459-
listOrgsUncachedSpy = spyOn(apiClient, "listOrganizationsUncached");
482+
listOrgsUncachedSpy = vi.spyOn(apiClient, "listOrganizationsUncached");
460483
listOrgsUncachedSpy.mockResolvedValue([]);
461484

462485
// Defaults
@@ -567,18 +590,18 @@ describe("login re-authentication interactive prompt", () => {
567590
getUserInfoSpy.mockReturnValue(undefined);
568591
mockPrompt.mockResolvedValue(true);
569592

570-
const setAuthTokenSpy = spyOn(dbAuth, "setAuthToken");
593+
const setAuthTokenSpy = vi.spyOn(dbAuth, "setAuthToken");
571594
setAuthTokenSpy.mockImplementation(noop);
572-
const getUserRegionsSpy = spyOn(apiClient, "getUserRegions");
595+
const getUserRegionsSpy = vi.spyOn(apiClient, "getUserRegions");
573596
getUserRegionsSpy.mockResolvedValue([]);
574-
const getCurrentUserSpy = spyOn(apiClient, "getCurrentUser");
597+
const getCurrentUserSpy = vi.spyOn(apiClient, "getCurrentUser");
575598
getCurrentUserSpy.mockResolvedValue({
576599
id: "42",
577600
name: "Jane",
578601
username: "jane",
579602
email: "jane@example.com",
580603
});
581-
const setUserInfoSpy = spyOn(dbUser, "setUserInfo");
604+
const setUserInfoSpy = vi.spyOn(dbUser, "setUserInfo");
582605
setUserInfoSpy.mockReturnValue(undefined);
583606

584607
const context = createPromptContext();

0 commit comments

Comments
 (0)