Skip to content

Commit 7382f4e

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 7382f4e

342 files changed

Lines changed: 3376 additions & 2387 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: 16 additions & 52 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: 74 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,7 +83,7 @@ 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
);
@@ -121,12 +128,12 @@ function createContext() {
121128
const stdoutChunks: string[] = [];
122129
const context = {
123130
stdout: {
124-
write: mock((s: string) => {
131+
write: vi.fn((s: string) => {
125132
stdoutChunks.push(s);
126133
}),
127134
},
128135
stderr: {
129-
write: mock((_s: string) => {
136+
write: vi.fn((_s: string) => {
130137
// unused — diagnostics go through logger
131138
}),
132139
},
@@ -165,19 +172,19 @@ describe("loginCommand.func --token path", () => {
165172
let func: LoginFunc;
166173

167174
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");
175+
isAuthenticatedSpy = vi.spyOn(dbAuth, "isAuthenticated");
176+
isEnvTokenActiveSpy = vi.spyOn(dbAuth, "isEnvTokenActive");
177+
setAuthTokenSpy = vi.spyOn(dbAuth, "setAuthToken");
178+
getUserRegionsSpy = vi.spyOn(apiClient, "getUserRegions");
179+
clearAuthSpy = vi.spyOn(dbAuth, "clearAuth");
180+
getCurrentUserSpy = vi.spyOn(apiClient, "getCurrentUser");
181+
setUserInfoSpy = vi.spyOn(dbUser, "setUserInfo");
182+
runInteractiveLoginSpy = vi.spyOn(interactiveLogin, "runInteractiveLogin");
183+
hasStoredAuthCredentialsSpy = vi.spyOn(dbAuth, "hasStoredAuthCredentials");
177184
// Prevent warmOrgCache() fire-and-forget from hitting real fetch.
178185
// After successful login, warmOrgCache() calls listOrganizationsUncached()
179186
// which triggers API calls that leak as "unexpected fetch" warnings.
180-
listOrgsUncachedSpy = spyOn(apiClient, "listOrganizationsUncached");
187+
listOrgsUncachedSpy = vi.spyOn(apiClient, "listOrganizationsUncached");
181188
listOrgsUncachedSpy.mockResolvedValue([]);
182189
isEnvTokenActiveSpy.mockReturnValue(false);
183190
hasStoredAuthCredentialsSpy.mockReturnValue(false);
@@ -429,7 +436,7 @@ describe("loginCommand.func --token path", () => {
429436
/**
430437
* Tests for the interactive TTY re-authentication prompt.
431438
*
432-
* Uses the module-level `mock.module()` on node:tty (so `isatty(0)` returns
439+
* Uses the module-level `vi.mock()` on node:tty (so `isatty(0)` returns
433440
* true) and the logger (so `.prompt()` is controllable).
434441
*/
435442
describe("login re-authentication interactive prompt", () => {
@@ -443,20 +450,20 @@ describe("login re-authentication interactive prompt", () => {
443450

444451
function createPromptContext() {
445452
return {
446-
stdout: { write: mock(() => true) },
447-
stderr: { write: mock(() => true) },
453+
stdout: { write: vi.fn(() => true) },
454+
stderr: { write: vi.fn(() => true) },
448455
cwd: "/tmp",
449456
};
450457
}
451458

452459
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");
460+
isAuthenticatedSpy = vi.spyOn(dbAuth, "isAuthenticated");
461+
isEnvTokenActiveSpy = vi.spyOn(dbAuth, "isEnvTokenActive");
462+
clearAuthSpy = vi.spyOn(dbAuth, "clearAuth");
463+
runInteractiveLoginSpy = vi.spyOn(interactiveLogin, "runInteractiveLogin");
464+
getUserInfoSpy = vi.spyOn(dbUser, "getUserInfo");
458465
// Prevent warmOrgCache() fire-and-forget from hitting real fetch.
459-
listOrgsUncachedSpy = spyOn(apiClient, "listOrganizationsUncached");
466+
listOrgsUncachedSpy = vi.spyOn(apiClient, "listOrganizationsUncached");
460467
listOrgsUncachedSpy.mockResolvedValue([]);
461468

462469
// Defaults
@@ -567,18 +574,18 @@ describe("login re-authentication interactive prompt", () => {
567574
getUserInfoSpy.mockReturnValue(undefined);
568575
mockPrompt.mockResolvedValue(true);
569576

570-
const setAuthTokenSpy = spyOn(dbAuth, "setAuthToken");
577+
const setAuthTokenSpy = vi.spyOn(dbAuth, "setAuthToken");
571578
setAuthTokenSpy.mockImplementation(noop);
572-
const getUserRegionsSpy = spyOn(apiClient, "getUserRegions");
579+
const getUserRegionsSpy = vi.spyOn(apiClient, "getUserRegions");
573580
getUserRegionsSpy.mockResolvedValue([]);
574-
const getCurrentUserSpy = spyOn(apiClient, "getCurrentUser");
581+
const getCurrentUserSpy = vi.spyOn(apiClient, "getCurrentUser");
575582
getCurrentUserSpy.mockResolvedValue({
576583
id: "42",
577584
name: "Jane",
578585
username: "jane",
579586
email: "jane@example.com",
580587
});
581-
const setUserInfoSpy = spyOn(dbUser, "setUserInfo");
588+
const setUserInfoSpy = vi.spyOn(dbUser, "setUserInfo");
582589
setUserInfoSpy.mockReturnValue(undefined);
583590

584591
const context = createPromptContext();

test/commands/auth/logout.test.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ import {
1313
beforeEach,
1414
describe,
1515
expect,
16-
mock,
17-
spyOn,
1816
test,
19-
} from "bun:test";
17+
vi,
18+
} from "vitest";
2019
import { logoutCommand } from "../../../src/commands/auth/logout.js";
2120
// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
2221
import * as dbAuth from "../../../src/lib/db/auth.js";
@@ -34,12 +33,12 @@ function createContext() {
3433
return {
3534
context: {
3635
stdout: {
37-
write: mock((s: string) => {
36+
write: vi.fn((s: string) => {
3837
stdoutChunks.push(s);
3938
}),
4039
},
4140
stderr: {
42-
write: mock((_s: string) => {
41+
write: vi.fn((_s: string) => {
4342
/* captured by mock */
4443
}),
4544
},
@@ -58,11 +57,11 @@ describe("logoutCommand.func", () => {
5857
let func: LogoutFunc;
5958

6059
beforeEach(async () => {
61-
isAuthenticatedSpy = spyOn(dbAuth, "isAuthenticated");
62-
isEnvTokenActiveSpy = spyOn(dbAuth, "isEnvTokenActive");
63-
getAuthConfigSpy = spyOn(dbAuth, "getAuthConfig");
64-
clearAuthSpy = spyOn(dbAuth, "clearAuth");
65-
getDbPathSpy = spyOn(dbIndex, "getDbPath");
60+
isAuthenticatedSpy = vi.spyOn(dbAuth, "isAuthenticated");
61+
isEnvTokenActiveSpy = vi.spyOn(dbAuth, "isEnvTokenActive");
62+
getAuthConfigSpy = vi.spyOn(dbAuth, "getAuthConfig");
63+
clearAuthSpy = vi.spyOn(dbAuth, "clearAuth");
64+
getDbPathSpy = vi.spyOn(dbIndex, "getDbPath");
6665

6766
clearAuthSpy.mockResolvedValue(undefined);
6867
getDbPathSpy.mockReturnValue("/fake/db/path");

0 commit comments

Comments
 (0)