chore: migrate monorepo toolchain pnpm+vitest → bun (bun test, bunfig preload, bun.lock)
- Gateway + backend + frontend: pnpm/vitest fully removed → bun 1.3.14 (bun install, bun test tests/, bunfig.toml [test] preload, bun.lock). - vitest configs deleted; vitest→bun facade (jest/mock/spyOn/waitForCompat) keeps the vitest-style assertions working under bun:test. - flake.nix: bunInstall switch; pruned prod-pass now removes post-pnpm dev-toolchain trees; frontend builds Next standalone via bun's next. - CI: deploy.yml installs with bun + runs bun test tests/ per service. - 190 tests green (146 gateway + 37 backend pass, 14 skip), 0 fail; tsc + biome across all 3 services clean. Co-authored-by: git-migration[bot] <noreply@gmw.local>
This commit is contained in:
co-authored by
git-migration[bot]
parent
7acc49e1fb
commit
7be069d73f
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
[test]
|
||||
preload = ["./tests/setup-env.ts"]
|
||||
@@ -5,7 +5,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"packageManager": "pnpm@11.20.0",
|
||||
"packageManager": "bun@1.3.14",
|
||||
"engines": {
|
||||
"node": ">=22.12.0",
|
||||
"pnpm": ">=9.0.0"
|
||||
@@ -16,8 +16,9 @@
|
||||
"format": "biome format --write .",
|
||||
"lint": "biome check --diagnostic-level=error .",
|
||||
"start": "node dist/index.js",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"test": "bun test tests/",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:e2e": "bun test src/e2e.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@orpc/server": "1.15.3",
|
||||
@@ -41,6 +42,6 @@
|
||||
"@types/ws": "^8.18.1",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "^7.0.2",
|
||||
"vitest": "^5.0.1"
|
||||
"@types/bun": "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-2680
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { tools } from "../src/modules/chatbot/chatbot.toolDefs.js";
|
||||
|
||||
const names = tools.map((t) => t.function.name);
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
// ─── Shared Error Classes ────────────────────────────────────────────────────
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
// bun:test compat facade — vitest's `vi` maps onto bun's `jest`/`mock`/`spyOn`.
|
||||
// bun:test 1.3.14 exports both `jest` (fn, useFakeTimers, spyOn) and `mock`
|
||||
// (module, restore). `vi.fn` -> `jest.fn`, `vi.useFakeTimers` -> `jest.useFakeTimers`,
|
||||
// `vi.waitFor` -> waitForCompat (poll until the assertion passes).
|
||||
|
||||
import { afterEach, describe, expect, it, jest } from "bun:test";
|
||||
|
||||
const useFakeTimers = () => jest.useFakeTimers();
|
||||
const useRealTimers = () => jest.useRealTimers();
|
||||
const advanceTimersByTime = (ms: number) => jest.advanceTimersByTime(ms);
|
||||
async function waitForCompat(fn: () => Promise<unknown>, timeoutMs = 2_000) {
|
||||
const start = Date.now();
|
||||
let lastErr: unknown;
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
await fn();
|
||||
return;
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
}
|
||||
}
|
||||
throw lastErr instanceof Error
|
||||
? lastErr
|
||||
: new Error("waitForCompat timed out");
|
||||
}
|
||||
|
||||
import {
|
||||
AppError,
|
||||
ConfigError,
|
||||
@@ -94,41 +120,41 @@ describe("AppError subclasses", () => {
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
describe("delay", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
useRealTimers();
|
||||
});
|
||||
|
||||
it("resolves after the given time", async () => {
|
||||
vi.useFakeTimers();
|
||||
useFakeTimers();
|
||||
const promise = delay(500);
|
||||
vi.advanceTimersByTime(500);
|
||||
advanceTimersByTime(500);
|
||||
await expect(promise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects are not triggered on non-matching timer", async () => {
|
||||
vi.useFakeTimers();
|
||||
useFakeTimers();
|
||||
const promise = delay(1000);
|
||||
// Advance only part way — the timer should NOT fire yet
|
||||
vi.advanceTimersByTime(500);
|
||||
advanceTimersByTime(500);
|
||||
// The timer is still pending; the promise has not resolved yet
|
||||
// We advance the rest
|
||||
vi.advanceTimersByTime(500);
|
||||
advanceTimersByTime(500);
|
||||
await expect(promise).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("retryWithBackoff", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
useRealTimers();
|
||||
});
|
||||
|
||||
it("returns the result on first success without retrying", async () => {
|
||||
const fn = vi.fn().mockResolvedValue("ok");
|
||||
const fn = jest.fn().mockResolvedValue("ok");
|
||||
await expect(retryWithBackoff(fn)).resolves.toBe("ok");
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("re-throws after exhausting all retries", async () => {
|
||||
const fn = vi.fn().mockRejectedValue(new Error("persistent"));
|
||||
const fn = jest.fn().mockRejectedValue(new Error("persistent"));
|
||||
await expect(
|
||||
retryWithBackoff(fn, { retries: 1, minTimeout: 1, maxTimeout: 5 }),
|
||||
).rejects.toThrow("persistent");
|
||||
@@ -139,7 +165,7 @@ describe("retryWithBackoff", () => {
|
||||
it("throws AbortError immediately when signal is already aborted", async () => {
|
||||
const ac = new AbortController();
|
||||
ac.abort();
|
||||
const fn = vi.fn().mockResolvedValue("ok");
|
||||
const fn = jest.fn().mockResolvedValue("ok");
|
||||
await expect(
|
||||
retryWithBackoff(fn, { retries: 3, signal: ac.signal }),
|
||||
).rejects.toThrow("Aborted");
|
||||
@@ -147,9 +173,9 @@ describe("retryWithBackoff", () => {
|
||||
});
|
||||
|
||||
it("respects abort signal during retry", async () => {
|
||||
vi.useFakeTimers();
|
||||
useFakeTimers();
|
||||
const ac = new AbortController();
|
||||
const fn = vi.fn().mockRejectedValue(new Error("fail"));
|
||||
const fn = jest.fn().mockRejectedValue(new Error("fail"));
|
||||
|
||||
const promise = retryWithBackoff(fn, {
|
||||
retries: 5,
|
||||
@@ -159,8 +185,8 @@ describe("retryWithBackoff", () => {
|
||||
|
||||
// Schedule abort after first failure + backoff starts
|
||||
setTimeout(() => ac.abort(), 150);
|
||||
vi.advanceTimersByTime(200);
|
||||
await vi.waitFor(async () => {
|
||||
advanceTimersByTime(200);
|
||||
await waitForCompat(async () => {
|
||||
await expect(promise).rejects.toThrow("Aborted");
|
||||
});
|
||||
});
|
||||
@@ -232,7 +258,7 @@ describe("asyncHandler", () => {
|
||||
const wrapped = asyncHandler(async () => {
|
||||
throw error;
|
||||
});
|
||||
const next = vi.fn();
|
||||
const next = jest.fn();
|
||||
|
||||
wrapped({} as any, {} as any, next);
|
||||
|
||||
@@ -246,7 +272,7 @@ describe("asyncHandler", () => {
|
||||
const wrapped = asyncHandler(async (_req: any, _res: any, _next: any) => {
|
||||
// no-op
|
||||
});
|
||||
const next = vi.fn();
|
||||
const next = jest.fn();
|
||||
|
||||
wrapped({} as any, {} as any, next);
|
||||
await Promise.resolve();
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
// bun test preload — nothing needed for backend unit tests today.
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it } from "bun:test";
|
||||
|
||||
/**
|
||||
* Lock the contract that the WS `stream_messages` handler + frontend
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
|
||||
testTimeout: 15000,
|
||||
},
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
[test]
|
||||
preload = ["./tests/setup-env.ts"]
|
||||
@@ -17,10 +17,8 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "biome check --diagnostic-level=error .",
|
||||
"format": "biome format --write .",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run --exclude \"tests/llmE2e.test.ts\"",
|
||||
"test:e2e": "vitest run tests/llmE2e.test.ts",
|
||||
"test:e2e:live": "bash scripts/run-llm-e2e.sh"
|
||||
"test": "bun test tests/",
|
||||
"test:e2e": "bun test tests/llmE2e.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.20.0",
|
||||
@@ -49,6 +47,7 @@
|
||||
"drizzle-kit": "^0.31.11",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "^7.0.2",
|
||||
"vitest": "latest"
|
||||
}
|
||||
}
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"packageManager": "bun@1.3.14"
|
||||
}
|
||||
Generated
-4098
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
||||
allowBuilds:
|
||||
"@discordjs/opus": true
|
||||
"@lng2004/node-datachannel": true
|
||||
esbuild: true
|
||||
node-av: true
|
||||
sharp: true
|
||||
zeromq: true
|
||||
# pnpm 11 requires build-script approvals here (the legacy `pnpm` field in
|
||||
# package.json is ignored). Native voice deps need their postinstall build.
|
||||
# NOTE: sharp sengaja TIDAK ada — binary-nya dari @img/sharp-linux-x64
|
||||
# (prebuilt), install script-nya cuma validasi dan gagal di Nix sandbox.
|
||||
# Kalau script sharp dijalankan pnpm rebuild abort sebelum opus/datachannel
|
||||
# kebangun. node-crc dihapus dari deps (tidak pernah di-import).
|
||||
onlyBuiltDependencies:
|
||||
- "@discordjs/opus"
|
||||
- "@lng2004/node-datachannel"
|
||||
- esbuild
|
||||
- node-av
|
||||
- zeromq
|
||||
@@ -14,10 +14,12 @@ export {
|
||||
downloadAndExtractFrame,
|
||||
sniffImageMimeType,
|
||||
} from "./mediaDownloader.js";
|
||||
export type {
|
||||
MessageImagePart,
|
||||
PreparedMediaMessage,
|
||||
} from "./visionAnalyzer.js";
|
||||
export {
|
||||
analyzeSingleMediaImage,
|
||||
hasMediaContent,
|
||||
MessageImagePart,
|
||||
PreparedMediaMessage,
|
||||
prepareMediaMessage,
|
||||
} from "./visionAnalyzer.js";
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
// single-key getter: unexpired rows only, malformed rows skipped, verdicts
|
||||
// normalized through the shared parser. The DB layer is mocked — no live
|
||||
// Postgres in unit tests.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, jest, mock } from "bun:test";
|
||||
|
||||
const executeAll = vi.fn();
|
||||
const executeGet = vi.fn();
|
||||
const executeAll = jest.fn();
|
||||
const executeGet = jest.fn();
|
||||
|
||||
vi.mock("../src/shared/database/drizzle.js", () => ({
|
||||
mock.module("../src/shared/database/drizzle.js", () => ({
|
||||
executeAll: (...args: unknown[]) => executeAll(...args),
|
||||
executeGet: (...args: unknown[]) => executeGet(...args),
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// bun test preload — replicates the env block from the old vitest.config.ts.
|
||||
// Runs before any test module imports, so the config singleton (which reads
|
||||
// process.env at import time) gets the same test values it had under vitest.
|
||||
process.env.DISCORD_TOKEN = "test-discord-token";
|
||||
process.env.DATABASE_URL = "postgres://localhost:6432/test";
|
||||
process.env.AI_ANALYSIS_ENABLED = "true";
|
||||
process.env.AI_LLM_API_KEY = "sk-test";
|
||||
@@ -45,7 +45,7 @@ describe("tinyFishSearch fallback", () => {
|
||||
// Pin the live config object to a known-disabled state: the shell may
|
||||
// export a real TINYFISH_API_KEY (dev box), which would flip
|
||||
// isTinyFishEnabled() and let tests hit the network.
|
||||
const { config } = await import("../../src/shared/config/index.js");
|
||||
const { config } = await import("../src/shared/config/index.js");
|
||||
prevKey = config.TINYFISH_API_KEY;
|
||||
prevEnabled = config.TINYFISH_SEARCH_ENABLED;
|
||||
(config as Record<string, unknown>).TINYFISH_API_KEY = "";
|
||||
@@ -54,7 +54,7 @@ describe("tinyFishSearch fallback", () => {
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
const { config } = await import("../../src/shared/config/index.js");
|
||||
const { config } = await import("../src/shared/config/index.js");
|
||||
(config as Record<string, unknown>).TINYFISH_API_KEY = prevKey;
|
||||
(config as Record<string, unknown>).TINYFISH_SEARCH_ENABLED = prevEnabled;
|
||||
});
|
||||
@@ -63,7 +63,7 @@ describe("tinyFishSearch fallback", () => {
|
||||
config: Record<string, unknown>;
|
||||
prev: string;
|
||||
}> {
|
||||
const { config } = await import("../../src/shared/config/index.js");
|
||||
const { config } = await import("../src/shared/config/index.js");
|
||||
const prev = config.TINYFISH_API_KEY;
|
||||
(config as Record<string, unknown>).TINYFISH_API_KEY = "sk-test-key";
|
||||
return { config: config as unknown as Record<string, unknown>, prev };
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
// Resolves the "@/*" tsconfig path alias so vitest can import src modules
|
||||
// (the pre-existing test suite was broken without this).
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
include: ["tests/**/*.test.ts"],
|
||||
// Loaded before module imports — satisfies the config singleton
|
||||
// (DISCORD_TOKEN required) and DB-agnostic pure-function tests.
|
||||
env: {
|
||||
DISCORD_TOKEN: "test-discord-token",
|
||||
DATABASE_URL: "postgres://localhost:6432/test",
|
||||
AI_ANALYSIS_ENABLED: "true",
|
||||
AI_LLM_API_KEY: "sk-test",
|
||||
},
|
||||
},
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
Generated
-2043
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
allowBuilds:
|
||||
sharp: set this to true or false
|
||||
# pnpm 11 requires build-script approvals here (the legacy `pnpm` field in
|
||||
# package.json is ignored). Only packages that genuinely need a postinstall
|
||||
# build are listed; everything else installs with scripts skipped.
|
||||
onlyBuiltDependencies:
|
||||
- sharp
|
||||
Reference in New Issue
Block a user