Refactor test database setup and add migrations
- Updated test files to use a separate test database configuration. - Introduced a new helper module for managing test database operations. - Added a setup file to configure the environment for tests. - Created new database migration scripts to optimize message indexing. - Added a sample environment file for test database configuration.
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
NODE_ENV=test
|
||||||
|
# Use a separate database/data area for tests. It may be on the same PostgreSQL host,
|
||||||
|
# but the database name must clearly be a test database so destructive test setup
|
||||||
|
# cannot touch production data.
|
||||||
|
TEST_DATABASE_URL=postgres://root:root@100.108.1.124:5432/hub_test
|
||||||
|
DATABASE_URL=postgres://root:root@100.108.1.124:5432/hub_test
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
CREATE INDEX IF NOT EXISTS "idx_messages_guild_created_deleted" ON "messages" USING btree ("guild_id","created_at","deleted_at","id");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_messages_channel_ai_status_created" ON "messages" USING btree ("channel_id","ai_status","created_at","id");--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_messages_thread_ai_status_created" ON "messages" USING btree ("thread_id","ai_status","created_at","id");
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,13 @@
|
|||||||
"when": 1780079000000,
|
"when": 1780079000000,
|
||||||
"tag": "0003_ai_moderation_review_guardrails",
|
"tag": "0003_ai_moderation_review_guardrails",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780218363790,
|
||||||
|
"tag": "0005_optimize-message-index",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -106,6 +106,18 @@ export const pgMessagesTable = pgTable(
|
|||||||
table.created_at,
|
table.created_at,
|
||||||
table.id,
|
table.id,
|
||||||
),
|
),
|
||||||
|
guildCreatedDeletedIdx: pgIndex("idx_messages_guild_created_deleted").on(
|
||||||
|
table.guild_id,
|
||||||
|
table.created_at,
|
||||||
|
table.deleted_at,
|
||||||
|
table.id,
|
||||||
|
),
|
||||||
|
channelAiStatusCreatedIdx: pgIndex(
|
||||||
|
"idx_messages_channel_ai_status_created",
|
||||||
|
).on(table.channel_id, table.ai_status, table.created_at, table.id),
|
||||||
|
threadAiStatusCreatedIdx: pgIndex(
|
||||||
|
"idx_messages_thread_ai_status_created",
|
||||||
|
).on(table.thread_id, table.ai_status, table.created_at, table.id),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -56,12 +56,14 @@ export async function buildConversationContext(
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const contextLines = await Promise.all(
|
||||||
|
contextBefore.map((msg) => formatMessageForPrompt(msg, "context")),
|
||||||
|
);
|
||||||
const selectedContextLines: string[] = [];
|
const selectedContextLines: string[] = [];
|
||||||
|
|
||||||
// Go backwards through context, taking most recent first
|
// Go backwards through context, taking most recent first
|
||||||
for (let i = contextBefore.length - 1; i >= 0; i--) {
|
for (let i = contextLines.length - 1; i >= 0; i--) {
|
||||||
const msg = contextBefore[i];
|
const line = contextLines[i];
|
||||||
const line = await formatMessageForPrompt(msg, "context");
|
|
||||||
const lineTokens = estimateTokens(line);
|
const lineTokens = estimateTokens(line);
|
||||||
|
|
||||||
if (usedTokens + lineTokens <= maxTokens) {
|
if (usedTokens + lineTokens <= maxTokens) {
|
||||||
|
|||||||
@@ -178,14 +178,27 @@ export async function captureMessage(
|
|||||||
// Queue analysis after attachment uploads settle so AI uses stable tele URLs.
|
// Queue analysis after attachment uploads settle so AI uses stable tele URLs.
|
||||||
if (!isBacklog) {
|
if (!isBacklog) {
|
||||||
if (attachmentUploadTasks.length > 0) {
|
if (attachmentUploadTasks.length > 0) {
|
||||||
setTimeout(() => queueMessageAnalysis(message.id), 30000);
|
let analysisQueued = false;
|
||||||
|
let fallbackTimer: NodeJS.Timeout | null = null;
|
||||||
|
const queueAnalysisOnce = () => {
|
||||||
|
if (analysisQueued) return;
|
||||||
|
analysisQueued = true;
|
||||||
|
if (fallbackTimer) {
|
||||||
|
clearTimeout(fallbackTimer);
|
||||||
|
fallbackTimer = null;
|
||||||
|
}
|
||||||
|
queueMessageAnalysis(message.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
fallbackTimer = setTimeout(queueAnalysisOnce, 30000);
|
||||||
Promise.allSettled(attachmentUploadTasks)
|
Promise.allSettled(attachmentUploadTasks)
|
||||||
.then(() => queueMessageAnalysis(message.id))
|
.then(queueAnalysisOnce)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
{ messageId: message.id, error: err },
|
{ messageId: message.id, error: err },
|
||||||
"Failed to queue message analysis after attachment upload",
|
"Failed to queue message analysis after attachment upload",
|
||||||
);
|
);
|
||||||
|
queueAnalysisOnce();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
queueMessageAnalysis(message.id);
|
queueMessageAnalysis(message.id);
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ interface MessageDatabase {
|
|||||||
selectDistinct<T = unknown[]>(...args: unknown[]): QueryBuilder<T>;
|
selectDistinct<T = unknown[]>(...args: unknown[]): QueryBuilder<T>;
|
||||||
insert<T = unknown>(...args: unknown[]): QueryBuilder<T>;
|
insert<T = unknown>(...args: unknown[]): QueryBuilder<T>;
|
||||||
update(...args: unknown[]): QueryBuilder<unknown>;
|
update(...args: unknown[]): QueryBuilder<unknown>;
|
||||||
|
transaction<T>(callback: (tx: MessageDatabase) => Promise<T>): Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function db(): MessageDatabase {
|
function db(): MessageDatabase {
|
||||||
@@ -457,28 +458,28 @@ export async function updateMessagesAIAnalysisBulk(
|
|||||||
): Promise<MessageRecord[]> {
|
): Promise<MessageRecord[]> {
|
||||||
if (updates.length === 0) return [];
|
if (updates.length === 0) return [];
|
||||||
try {
|
try {
|
||||||
// Use raw SQL batch UPDATE instead of Promise.all per-message queries
|
|
||||||
// (P2: reduce N*2 queries → 2 queries total)
|
|
||||||
const database = db();
|
const database = db();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
for (const { messageId, result } of updates) {
|
await database.transaction(async (tx) => {
|
||||||
await database
|
for (const { messageId, result } of updates) {
|
||||||
.update(messagesTable)
|
await tx
|
||||||
.set({
|
.update(messagesTable)
|
||||||
ai_status: result.status,
|
.set({
|
||||||
ai_moderation_flags: result.flags ?? null,
|
ai_status: result.status,
|
||||||
ai_moderation_score: result.score ?? null,
|
ai_moderation_flags: result.flags ?? null,
|
||||||
ai_analysis: result.analysis ?? null,
|
ai_moderation_score: result.score ?? null,
|
||||||
ai_categories: stringifyAIList(result.categories),
|
ai_analysis: result.analysis ?? null,
|
||||||
ai_severity: result.severity ?? null,
|
ai_categories: stringifyAIList(result.categories),
|
||||||
ai_confidence: result.confidence ?? result.score ?? null,
|
ai_severity: result.severity ?? null,
|
||||||
ai_recommended_action: result.recommendedAction ?? null,
|
ai_confidence: result.confidence ?? result.score ?? null,
|
||||||
ai_analyzed_at: result.analyzedAt ?? now,
|
ai_recommended_action: result.recommendedAction ?? null,
|
||||||
ai_error: result.error ?? null,
|
ai_analyzed_at: result.analyzedAt ?? now,
|
||||||
})
|
ai_error: result.error ?? null,
|
||||||
.where(eq(messagesTable.id, messageId));
|
})
|
||||||
}
|
.where(eq(messagesTable.id, messageId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Fetch all updated messages in a single query
|
// Fetch all updated messages in a single query
|
||||||
const ids = updates.map(({ messageId }) => messageId);
|
const ids = updates.map(({ messageId }) => messageId);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export async function uploadRecordingSegment(input: {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Get file size and insert initial pending state to DB
|
// 1. Get file size and insert initial pending state to DB
|
||||||
const stats = fs.statSync(oggPath);
|
const stats = await fs.promises.stat(oggPath);
|
||||||
await insertVoiceRecording({
|
await insertVoiceRecording({
|
||||||
id,
|
id,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
@@ -54,7 +54,7 @@ export async function uploadRecordingSegment(input: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 2. Perform async upload with retry logic
|
// 2. Perform async upload with retry logic
|
||||||
const fileBuffer = fs.readFileSync(oggPath);
|
const fileBuffer = await fs.promises.readFile(oggPath);
|
||||||
const uploadResult = await uploadToTele({
|
const uploadResult = await uploadToTele({
|
||||||
buffer: fileBuffer,
|
buffer: fileBuffer,
|
||||||
filename: fileName,
|
filename: fileName,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
import { assertSafeTestDatabaseUrl } from "./helpers/testDatabase";
|
||||||
|
|
||||||
const originalEnv = process.env;
|
const originalEnv = process.env;
|
||||||
|
|
||||||
@@ -14,6 +15,10 @@ describe("Drizzle ORM Database", () => {
|
|||||||
DISCORD_TOKEN: "test-token",
|
DISCORD_TOKEN: "test-token",
|
||||||
NODE_ENV: "test",
|
NODE_ENV: "test",
|
||||||
};
|
};
|
||||||
|
if (originalEnv.TEST_DATABASE_URL) {
|
||||||
|
process.env.DATABASE_URL = originalEnv.TEST_DATABASE_URL;
|
||||||
|
}
|
||||||
|
assertSafeTestDatabaseUrl();
|
||||||
|
|
||||||
// Reset modules to pick up new environment
|
// Reset modules to pick up new environment
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import process from "node:process";
|
||||||
|
import {
|
||||||
|
getDatabase,
|
||||||
|
initializeDatabase,
|
||||||
|
} from "../../src/database/drizzle";
|
||||||
|
|
||||||
|
interface RunnableDatabase {
|
||||||
|
run(sql: string): Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SAFE_TEST_DATABASE_NAME = /(^|[_-])(test|testing)([_-]|$)|gmw_test/i;
|
||||||
|
|
||||||
|
function getDatabaseNameFromUrl(databaseUrl: string): string {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(databaseUrl);
|
||||||
|
return parsed.pathname.replace(/^\//, "");
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfiguredDatabaseName(): string {
|
||||||
|
if (process.env.DATABASE_URL) {
|
||||||
|
return getDatabaseNameFromUrl(process.env.DATABASE_URL);
|
||||||
|
}
|
||||||
|
return process.env.POSTGRES_DB ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertSafeTestDatabaseUrl(): void {
|
||||||
|
if (process.env.NODE_ENV !== "test") {
|
||||||
|
throw new Error(
|
||||||
|
`Refusing to run destructive database test outside NODE_ENV=test (got ${process.env.NODE_ENV ?? "unset"})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.TEST_DATABASE_URL) {
|
||||||
|
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const databaseName = getConfiguredDatabaseName();
|
||||||
|
if (!SAFE_TEST_DATABASE_NAME.test(databaseName)) {
|
||||||
|
throw new Error(
|
||||||
|
`Refusing to run destructive database test against non-test database "${databaseName || "unknown"}". Set TEST_DATABASE_URL or DATABASE_URL to a database whose name contains "test" (for example hub_test).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initializeTestDatabase() {
|
||||||
|
assertSafeTestDatabaseUrl();
|
||||||
|
return initializeDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTestDatabase(): RunnableDatabase {
|
||||||
|
assertSafeTestDatabaseUrl();
|
||||||
|
return getDatabase() as unknown as RunnableDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearTestTables(...tableNames: string[]): Promise<void> {
|
||||||
|
assertSafeTestDatabaseUrl();
|
||||||
|
const db = getTestDatabase();
|
||||||
|
for (const tableName of tableNames) {
|
||||||
|
await db.run(`DELETE FROM "${tableName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -686,9 +686,7 @@ describe("runModerationAnalysis", () => {
|
|||||||
expect(secondRequestBody.messages[0].content).toContain(
|
expect(secondRequestBody.messages[0].content).toContain(
|
||||||
"RESPON SEBELUMNYA GAGAL VALIDASI",
|
"RESPON SEBELUMNYA GAGAL VALIDASI",
|
||||||
);
|
);
|
||||||
expect(secondRequestBody.messages[0].content).toContain(
|
expect(secondRequestBody.messages[0].content).toContain("Invalid option");
|
||||||
"Invalid option",
|
|
||||||
);
|
|
||||||
expect(secondRequestBody.messages[0].content).toContain(
|
expect(secondRequestBody.messages[0].content).toContain(
|
||||||
"Coba lagi dengan output JSON yang benar",
|
"Coba lagi dengan output JSON yang benar",
|
||||||
);
|
);
|
||||||
@@ -753,8 +751,8 @@ describe("runModerationAnalysis", () => {
|
|||||||
arrayBuffer: async () => {
|
arrayBuffer: async () => {
|
||||||
// Minimal valid PNG bytes (8-byte signature)
|
// Minimal valid PNG bytes (8-byte signature)
|
||||||
const png = Buffer.from([
|
const png = Buffer.from([
|
||||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
0x0d, 0x49, 0x48, 0x44, 0x52,
|
||||||
]);
|
]);
|
||||||
return png.buffer.slice(
|
return png.buffer.slice(
|
||||||
png.byteOffset,
|
png.byteOffset,
|
||||||
@@ -932,7 +930,8 @@ describe("runModerationAnalysis", () => {
|
|||||||
status: "warn",
|
status: "warn",
|
||||||
flags: ["harassment"],
|
flags: ["harassment"],
|
||||||
score: 0.65,
|
score: 0.65,
|
||||||
analysis: "Teks mengandung unsur harassment dan memerlukan tindakan lebih lanjut.",
|
analysis:
|
||||||
|
"Teks mengandung unsur harassment dan memerlukan tindakan lebih lanjut.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -941,7 +940,22 @@ describe("runModerationAnalysis", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const imageBytes = Buffer.from("realistic-image-bytes");
|
const imageBytes = Buffer.from([
|
||||||
|
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,
|
||||||
|
]);
|
||||||
|
const toBody = (buffer: Buffer) => {
|
||||||
|
let done = false;
|
||||||
|
return {
|
||||||
|
getReader: () => ({
|
||||||
|
read: async () => {
|
||||||
|
if (done) return { done: true, value: undefined };
|
||||||
|
done = true;
|
||||||
|
return { done: false, value: new Uint8Array(buffer) };
|
||||||
|
},
|
||||||
|
cancel: vi.fn(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
global.fetch = vi.fn().mockImplementation((url: string) => {
|
global.fetch = vi.fn().mockImplementation((url: string) => {
|
||||||
if (
|
if (
|
||||||
url === "https://httpbin.org/image/png" ||
|
url === "https://httpbin.org/image/png" ||
|
||||||
@@ -949,11 +963,7 @@ describe("runModerationAnalysis", () => {
|
|||||||
) {
|
) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
ok: true,
|
ok: true,
|
||||||
arrayBuffer: async () =>
|
body: toBody(imageBytes),
|
||||||
imageBytes.buffer.slice(
|
|
||||||
imageBytes.byteOffset,
|
|
||||||
imageBytes.byteOffset + imageBytes.byteLength,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import {
|
|||||||
it,
|
it,
|
||||||
vi,
|
vi,
|
||||||
} from "vitest";
|
} from "vitest";
|
||||||
|
import { closeDatabase } from "../../src/database/drizzle";
|
||||||
import {
|
import {
|
||||||
closeDatabase,
|
clearTestTables,
|
||||||
getDatabase,
|
getTestDatabase,
|
||||||
initializeDatabase,
|
initializeTestDatabase,
|
||||||
} from "../../src/database/drizzle";
|
} from "../helpers/testDatabase";
|
||||||
import { captureMessage } from "../../src/moderation/messageCapture";
|
import { captureMessage } from "../../src/moderation/messageCapture";
|
||||||
import type { ModerationBroadcaster } from "../../src/moderation/types";
|
import type { ModerationBroadcaster } from "../../src/moderation/types";
|
||||||
|
|
||||||
@@ -22,14 +23,6 @@ type ModerationTestGlobal = typeof globalThis & {
|
|||||||
moderationBroadcaster?: Partial<ModerationBroadcaster>;
|
moderationBroadcaster?: Partial<ModerationBroadcaster>;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface TestDatabase {
|
|
||||||
run(sql: string): Promise<unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTestDatabase(): TestDatabase {
|
|
||||||
return getDatabase() as unknown as TestDatabase;
|
|
||||||
}
|
|
||||||
|
|
||||||
vi.mock("../../src/moderation/aiAnalyzer", () => ({
|
vi.mock("../../src/moderation/aiAnalyzer", () => ({
|
||||||
queueMessageAnalysis: (id: string) => queueMessageAnalysis(id),
|
queueMessageAnalysis: (id: string) => queueMessageAnalysis(id),
|
||||||
}));
|
}));
|
||||||
@@ -118,15 +111,13 @@ async function createTables() {
|
|||||||
|
|
||||||
describe("captureMessage", () => {
|
describe("captureMessage", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await initializeDatabase();
|
await initializeTestDatabase();
|
||||||
await createTables();
|
await createTables();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
queueMessageAnalysis.mockClear();
|
queueMessageAnalysis.mockClear();
|
||||||
const db = getTestDatabase();
|
await clearTestTables("attachments", "messages");
|
||||||
await db.run(`DELETE FROM "attachments"`);
|
|
||||||
await db.run(`DELETE FROM "messages"`);
|
|
||||||
delete (globalThis as ModerationTestGlobal).moderationBroadcaster;
|
delete (globalThis as ModerationTestGlobal).moderationBroadcaster;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { closeDatabase } from "../../src/database/drizzle";
|
||||||
import {
|
import {
|
||||||
closeDatabase,
|
clearTestTables,
|
||||||
getDatabase,
|
getTestDatabase,
|
||||||
initializeDatabase,
|
initializeTestDatabase,
|
||||||
} from "../../src/database/drizzle";
|
} from "../helpers/testDatabase";
|
||||||
import { createChildLogger } from "../../src/logger";
|
import { createChildLogger } from "../../src/logger";
|
||||||
import {
|
import {
|
||||||
decodeCursor,
|
decodeCursor,
|
||||||
@@ -18,14 +19,6 @@ import {
|
|||||||
} from "../../src/moderation/messageStore";
|
} from "../../src/moderation/messageStore";
|
||||||
import type { MessageRecord } from "../../src/moderation/types";
|
import type { MessageRecord } from "../../src/moderation/types";
|
||||||
|
|
||||||
interface TestDatabase {
|
|
||||||
run(sql: string): Promise<unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTestDatabase(): TestDatabase {
|
|
||||||
return getDatabase() as unknown as TestDatabase;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logger = createChildLogger("messageStoreQueries.test");
|
const logger = createChildLogger("messageStoreQueries.test");
|
||||||
|
|
||||||
describe("message cursor helpers", () => {
|
describe("message cursor helpers", () => {
|
||||||
@@ -44,7 +37,7 @@ describe("message cursor helpers", () => {
|
|||||||
|
|
||||||
describe("message query integration tests", () => {
|
describe("message query integration tests", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await initializeDatabase();
|
await initializeTestDatabase();
|
||||||
// Create tables directly for isolated query integration tests
|
// Create tables directly for isolated query integration tests
|
||||||
const db = getTestDatabase();
|
const db = getTestDatabase();
|
||||||
try {
|
try {
|
||||||
@@ -110,9 +103,7 @@ describe("message query integration tests", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Clear tables before each test
|
// Clear tables before each test
|
||||||
try {
|
try {
|
||||||
const db = getTestDatabase();
|
await clearTestTables("attachments", "messages");
|
||||||
await db.run(`DELETE FROM "attachments"`);
|
|
||||||
await db.run(`DELETE FROM "messages"`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug({ error }, "Could not clear tables");
|
logger.debug({ error }, "Could not clear tables");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import process from "node:process";
|
||||||
|
|
||||||
|
process.env.NODE_ENV = "test";
|
||||||
|
|
||||||
|
if (process.env.TEST_DATABASE_URL) {
|
||||||
|
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
|
||||||
|
}
|
||||||
@@ -8,5 +8,6 @@ export default defineConfig({
|
|||||||
environment: "node",
|
environment: "node",
|
||||||
fileParallelism: false,
|
fileParallelism: false,
|
||||||
include: ["tests/**/*.test.ts"],
|
include: ["tests/**/*.test.ts"],
|
||||||
|
setupFiles: ["tests/setup.ts"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user