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:
MythEclipse
2026-05-31 16:32:38 +07:00
parent 8a712cff9b
commit f224be2a66
16 changed files with 1639 additions and 70 deletions
+12
View File
@@ -106,6 +106,18 @@ export const pgMessagesTable = pgTable(
table.created_at,
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),
}),
);
+5 -3
View File
@@ -56,12 +56,14 @@ export async function buildConversationContext(
0,
);
const contextLines = await Promise.all(
contextBefore.map((msg) => formatMessageForPrompt(msg, "context")),
);
const selectedContextLines: string[] = [];
// Go backwards through context, taking most recent first
for (let i = contextBefore.length - 1; i >= 0; i--) {
const msg = contextBefore[i];
const line = await formatMessageForPrompt(msg, "context");
for (let i = contextLines.length - 1; i >= 0; i--) {
const line = contextLines[i];
const lineTokens = estimateTokens(line);
if (usedTokens + lineTokens <= maxTokens) {
+15 -2
View File
@@ -178,14 +178,27 @@ export async function captureMessage(
// Queue analysis after attachment uploads settle so AI uses stable tele URLs.
if (!isBacklog) {
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)
.then(() => queueMessageAnalysis(message.id))
.then(queueAnalysisOnce)
.catch((err) => {
logger.error(
{ messageId: message.id, error: err },
"Failed to queue message analysis after attachment upload",
);
queueAnalysisOnce();
});
} else {
queueMessageAnalysis(message.id);
+20 -19
View File
@@ -48,6 +48,7 @@ interface MessageDatabase {
selectDistinct<T = unknown[]>(...args: unknown[]): QueryBuilder<T>;
insert<T = unknown>(...args: unknown[]): QueryBuilder<T>;
update(...args: unknown[]): QueryBuilder<unknown>;
transaction<T>(callback: (tx: MessageDatabase) => Promise<T>): Promise<T>;
}
function db(): MessageDatabase {
@@ -457,28 +458,28 @@ export async function updateMessagesAIAnalysisBulk(
): Promise<MessageRecord[]> {
if (updates.length === 0) return [];
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 now = Date.now();
for (const { messageId, result } of updates) {
await database
.update(messagesTable)
.set({
ai_status: result.status,
ai_moderation_flags: result.flags ?? null,
ai_moderation_score: result.score ?? null,
ai_analysis: result.analysis ?? null,
ai_categories: stringifyAIList(result.categories),
ai_severity: result.severity ?? null,
ai_confidence: result.confidence ?? result.score ?? null,
ai_recommended_action: result.recommendedAction ?? null,
ai_analyzed_at: result.analyzedAt ?? now,
ai_error: result.error ?? null,
})
.where(eq(messagesTable.id, messageId));
}
await database.transaction(async (tx) => {
for (const { messageId, result } of updates) {
await tx
.update(messagesTable)
.set({
ai_status: result.status,
ai_moderation_flags: result.flags ?? null,
ai_moderation_score: result.score ?? null,
ai_analysis: result.analysis ?? null,
ai_categories: stringifyAIList(result.categories),
ai_severity: result.severity ?? null,
ai_confidence: result.confidence ?? result.score ?? null,
ai_recommended_action: result.recommendedAction ?? null,
ai_analyzed_at: result.analyzedAt ?? now,
ai_error: result.error ?? null,
})
.where(eq(messagesTable.id, messageId));
}
});
// Fetch all updated messages in a single query
const ids = updates.map(({ messageId }) => messageId);
+2 -2
View File
@@ -38,7 +38,7 @@ export async function uploadRecordingSegment(input: {
try {
// 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({
id,
user_id: userId,
@@ -54,7 +54,7 @@ export async function uploadRecordingSegment(input: {
});
// 2. Perform async upload with retry logic
const fileBuffer = fs.readFileSync(oggPath);
const fileBuffer = await fs.promises.readFile(oggPath);
const uploadResult = await uploadToTele({
buffer: fileBuffer,
filename: fileName,