fix: reanalyze edited messages
This commit is contained in:
1
.claude/worktrees/agent-a3d811850036f998a
Submodule
1
.claude/worktrees/agent-a3d811850036f998a
Submodule
Submodule .claude/worktrees/agent-a3d811850036f998a added at 49d4bbf781
1
.claude/worktrees/agent-aa8b5c864c11af9a4
Submodule
1
.claude/worktrees/agent-aa8b5c864c11af9a4
Submodule
Submodule .claude/worktrees/agent-aa8b5c864c11af9a4 added at 49d4bbf781
1
.claude/worktrees/agent-aaadb24c021c240fb
Submodule
1
.claude/worktrees/agent-aaadb24c021c240fb
Submodule
Submodule .claude/worktrees/agent-aaadb24c021c240fb added at 49d4bbf781
1
.claude/worktrees/agent-abdbb74fab15cf706
Submodule
1
.claude/worktrees/agent-abdbb74fab15cf706
Submodule
Submodule .claude/worktrees/agent-abdbb74fab15cf706 added at 49d4bbf781
1
.claude/worktrees/agent-ace5d9f61d75c84e6
Submodule
1
.claude/worktrees/agent-ace5d9f61d75c84e6
Submodule
Submodule .claude/worktrees/agent-ace5d9f61d75c84e6 added at 49d4bbf781
1
.claude/worktrees/agent-ad5e79034c2a9c199
Submodule
1
.claude/worktrees/agent-ad5e79034c2a9c199
Submodule
Submodule .claude/worktrees/agent-ad5e79034c2a9c199 added at e3c48365c7
1
.claude/worktrees/agent-af0a2804fbe1b727c
Submodule
1
.claude/worktrees/agent-af0a2804fbe1b727c
Submodule
Submodule .claude/worktrees/agent-af0a2804fbe1b727c added at 49d4bbf781
30
.env.test
Normal file
30
.env.test
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
DISCORD_TOKEN=test_token_for_testing
|
||||||
|
RECORDINGS_DIR=./recordings
|
||||||
|
RECORDING_SEGMENT_MS=5000
|
||||||
|
VERBOSE=false
|
||||||
|
DECODER_ROTATE_MS=5000
|
||||||
|
DECODER_COOLDOWN_MS=30000
|
||||||
|
AUDIO_STREAM_SILENCE_DURATION_MS=3000
|
||||||
|
PACKET_FILTER_MIN_SIZE=8
|
||||||
|
OPUS_FRAME_SIZE=960
|
||||||
|
AUDIO_SAMPLE_RATE=48000
|
||||||
|
AUDIO_CHANNELS=2
|
||||||
|
AVATAR_SIZE=64
|
||||||
|
WEBSERVER_PORT=3000
|
||||||
|
VOICE_CONNECTION_TIMEOUT_MS=15000
|
||||||
|
RECONNECT_TIMEOUT_MS=5000
|
||||||
|
LOG_LEVEL=info
|
||||||
|
NODE_ENV=test
|
||||||
|
MONITOR_GUILD_ID=test_guild_id
|
||||||
|
PICSER_UPLOAD_URL=https://picser.asepharyana.tech/api/upload
|
||||||
|
ATTACHMENT_UPLOAD_TIMEOUT_MS=30000
|
||||||
|
ATTACHMENT_MAX_SIZE_MB=100
|
||||||
|
ATTACHMENT_RETRY_ATTEMPTS=3
|
||||||
|
BACKLOG_SYNC_HOURS=24
|
||||||
|
BACKLOG_SYNC_BATCH_SIZE=100
|
||||||
|
AI_ANALYSIS_ENABLED=false
|
||||||
|
AI_LLM_API_KEY=test_key
|
||||||
|
AI_LLM_BASE_URL=https://9router.asepharyana.tech/v1
|
||||||
|
AI_LLM_MODEL=free
|
||||||
|
AI_ANALYSIS_TIMEOUT_MS=30000
|
||||||
|
DATABASE_TYPE=sqlite
|
||||||
1854
docs/superpowers/plans/2026-05-14-ai-message-flow-react-dashboard.md
Normal file
1854
docs/superpowers/plans/2026-05-14-ai-message-flow-react-dashboard.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,264 @@
|
|||||||
|
# AI Message Flow + React Dashboard Redesign
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Rebuild the moderation watcher flow so message capture is fast, AI analysis is contextual and reliable, APIs are split by concern, and the dashboard is maintainable. The implementation may fully replace the current static frontend and reorganize backend modules, while preserving existing voice functionality.
|
||||||
|
|
||||||
|
## Current Problems
|
||||||
|
|
||||||
|
- Message capture, AI queuing, DB access, and WebSocket broadcasting are tightly coupled.
|
||||||
|
- AI analysis batches depend on array ordering, which can mismatch results when the model returns malformed or partial JSON.
|
||||||
|
- Pending analysis is polled globally, not grouped by conversation, so context is weak and request efficiency is inconsistent.
|
||||||
|
- `/api/messages` mixes text/image concerns and uses offset pagination, which gets slower and less stable as rows grow.
|
||||||
|
- Frontend is a large static HTML file with inline state, API, WebSocket, rendering, and audio code.
|
||||||
|
- WebSocket broadcast uses untyped `globalThis` hooks across modules.
|
||||||
|
|
||||||
|
## Backend Architecture
|
||||||
|
|
||||||
|
### Message ingestion
|
||||||
|
|
||||||
|
`messageCapture` becomes a narrow ingestion layer:
|
||||||
|
|
||||||
|
1. Filter Discord events by guild and author.
|
||||||
|
2. Normalize message payload and metadata.
|
||||||
|
3. Upsert message/attachment records.
|
||||||
|
4. Set or reset `ai_status` to `pending` for new or edited text.
|
||||||
|
5. Emit typed domain events for WebSocket broadcasting and analysis queueing.
|
||||||
|
|
||||||
|
It should not build prompts, manage AI batches, or query unrelated DB state.
|
||||||
|
|
||||||
|
### Store/repository layer
|
||||||
|
|
||||||
|
`messageStore` becomes the single message/attachment query boundary. It should expose focused functions:
|
||||||
|
|
||||||
|
- `upsertMessage`
|
||||||
|
- `markMessageEdited`
|
||||||
|
- `markMessageDeleted`
|
||||||
|
- `listMessages`
|
||||||
|
- `listReviewMessages`
|
||||||
|
- `getConversationContext`
|
||||||
|
- `claimPendingMessagesForChannel`
|
||||||
|
- `saveAnalysisResults`
|
||||||
|
- `insertAttachments`
|
||||||
|
|
||||||
|
Queries should use cursor pagination based on `(created_at, id)` instead of offset pagination. Common filters: `guildId`, `channelId`, `threadId`, `status`, `userId`, `q`, `limit`, `cursor`.
|
||||||
|
|
||||||
|
### Analysis queue
|
||||||
|
|
||||||
|
Add an `analysisQueue` module. It owns async AI processing and keeps capture fast.
|
||||||
|
|
||||||
|
- Queue key: `thread_id ?? channel_id`.
|
||||||
|
- Debounce: 1–3 seconds per key to group nearby messages.
|
||||||
|
- Batch pending messages by conversation key and token budget.
|
||||||
|
- Only one or a small fixed number of active LLM requests.
|
||||||
|
- Backlog worker feeds the same queue; no separate analysis path.
|
||||||
|
- Edits reset a message to `pending` and enqueue its conversation key.
|
||||||
|
|
||||||
|
If the process restarts, pending rows are recovered by a periodic lightweight scanner grouped by conversation key.
|
||||||
|
|
||||||
|
### Conversation context builder
|
||||||
|
|
||||||
|
Add `conversationContext` module:
|
||||||
|
|
||||||
|
- Input: conversation key + target pending messages.
|
||||||
|
- Fetch context before the first target message, normally 20 prior messages.
|
||||||
|
- Include target messages and close neighboring messages when within budget.
|
||||||
|
- Mark target messages explicitly in the prompt.
|
||||||
|
- Keep context scoped to one channel/thread to avoid irrelevant noise.
|
||||||
|
|
||||||
|
### LLM moderation client
|
||||||
|
|
||||||
|
Add `llmModerationClient` module:
|
||||||
|
|
||||||
|
- Own request shape, timeout, retry, JSON extraction, and validation.
|
||||||
|
- Prompt returns JSON keyed by `message_id`, not positional arrays.
|
||||||
|
- Expected response shape:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"message_id": "string",
|
||||||
|
"status": "clean|warn|flagged",
|
||||||
|
"flags": ["string"],
|
||||||
|
"score": 0.0,
|
||||||
|
"analysis": "Bahasa Indonesia summary + reason + suggested action"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Reject unknown IDs, invalid statuses, invalid scores, and missing target IDs.
|
||||||
|
- On partial model failure, retry once with smaller batch. If still invalid, mark only affected target messages as `error`.
|
||||||
|
- Store raw batch request/response in one run record if the DB migration is included; otherwise store compact raw metadata per message.
|
||||||
|
|
||||||
|
## API Design
|
||||||
|
|
||||||
|
Split API by use case so reads remain fast and obvious.
|
||||||
|
|
||||||
|
### Message read APIs
|
||||||
|
|
||||||
|
- `GET /api/messages`
|
||||||
|
- Query: `guildId`, `channelId`, `threadId`, `cursor`, `limit`, `status`, `userId`, `q`.
|
||||||
|
- Returns: `{ data, nextCursor }`.
|
||||||
|
- Uses indexed cursor pagination.
|
||||||
|
|
||||||
|
- `GET /api/messages/:id`
|
||||||
|
- Returns one message with attachments and AI analysis.
|
||||||
|
|
||||||
|
- `GET /api/review`
|
||||||
|
- Query: `guildId`, optional `channelId`, `status=warn,flagged,error`, `cursor`, `limit`.
|
||||||
|
- Optimized for moderator review panel.
|
||||||
|
|
||||||
|
- `GET /api/attachments`
|
||||||
|
- Query: `channelId`, `threadId`, `cursor`, `limit`, `type`.
|
||||||
|
- Replaces image mode inside `/api/messages`.
|
||||||
|
|
||||||
|
### Analysis APIs
|
||||||
|
|
||||||
|
- `POST /api/messages/:id/reanalyze`
|
||||||
|
- Sets message to `pending` and queues its conversation.
|
||||||
|
- Returns `202 Accepted` with current message status.
|
||||||
|
|
||||||
|
- `POST /api/analysis/requeue-pending`
|
||||||
|
- Admin/manual recovery endpoint for pending/error rows.
|
||||||
|
- Returns count queued.
|
||||||
|
|
||||||
|
- `GET /api/analysis/status`
|
||||||
|
- Returns queue depth, active requests, last error, and pending counts.
|
||||||
|
|
||||||
|
### Discord sync APIs
|
||||||
|
|
||||||
|
- `POST /api/backlog-sync`
|
||||||
|
- Stays async-friendly: starts sync for guild/channel/thread and returns `202` with a job id or immediate summary if small.
|
||||||
|
- Sync inserts messages, then queues analysis through the same `analysisQueue`.
|
||||||
|
|
||||||
|
### Voice/control APIs
|
||||||
|
|
||||||
|
Keep existing voice APIs working, but move route registration into route modules:
|
||||||
|
|
||||||
|
- `routes/voiceRoutes.ts`
|
||||||
|
- `routes/messageRoutes.ts`
|
||||||
|
- `routes/analysisRoutes.ts`
|
||||||
|
- `routes/syncRoutes.ts`
|
||||||
|
- `routes/uiStateRoutes.ts`
|
||||||
|
|
||||||
|
`webserver.ts` should only create Express/WS server, install middleware, register routes, and start listening.
|
||||||
|
|
||||||
|
## WebSocket Design
|
||||||
|
|
||||||
|
Replace ad-hoc globals with a typed broadcaster module.
|
||||||
|
|
||||||
|
Events:
|
||||||
|
|
||||||
|
- `ui_state`
|
||||||
|
- `user_state`
|
||||||
|
- `message_created`
|
||||||
|
- `message_updated`
|
||||||
|
- `message_deleted`
|
||||||
|
- `message_analyzed`
|
||||||
|
- `attachment_created`
|
||||||
|
- `analysis_queue_status`
|
||||||
|
|
||||||
|
Backend modules call broadcaster functions; they do not touch WebSocket clients directly.
|
||||||
|
|
||||||
|
## Database Changes
|
||||||
|
|
||||||
|
Add or verify indexes:
|
||||||
|
|
||||||
|
- messages `(channel_id, created_at, id)`
|
||||||
|
- messages `(thread_id, created_at, id)`
|
||||||
|
- messages `(ai_status, created_at, id)`
|
||||||
|
- messages `(guild_id, ai_status, created_at, id)`
|
||||||
|
- attachments `(channel_id, created_at, id)`
|
||||||
|
- attachments `(thread_id, created_at, id)`
|
||||||
|
|
||||||
|
Optional but preferred:
|
||||||
|
|
||||||
|
- `ai_analysis_runs` table:
|
||||||
|
- `id`
|
||||||
|
- `conversation_key`
|
||||||
|
- `target_message_ids`
|
||||||
|
- `model`
|
||||||
|
- `request_tokens_estimate`
|
||||||
|
- `response_raw`
|
||||||
|
- `status`
|
||||||
|
- `error`
|
||||||
|
- `created_at`
|
||||||
|
- `completed_at`
|
||||||
|
|
||||||
|
This avoids duplicating large raw LLM responses into every message row.
|
||||||
|
|
||||||
|
## React/Vite Frontend
|
||||||
|
|
||||||
|
Replace static inline dashboard code with a TypeScript React app.
|
||||||
|
|
||||||
|
Suggested structure:
|
||||||
|
|
||||||
|
- `frontend/src/api/` — typed REST clients
|
||||||
|
- `frontend/src/ws/` — WebSocket client and event types
|
||||||
|
- `frontend/src/state/` — small hooks for selected guild/channel, messages, review queue, voice state
|
||||||
|
- `frontend/src/components/voice/` — existing voice control/audio components
|
||||||
|
- `frontend/src/components/messages/` — feed, message card, filters, detail drawer
|
||||||
|
- `frontend/src/components/review/` — needs-review list and analysis status
|
||||||
|
- `frontend/src/components/layout/` — shell/sidebar/status cards
|
||||||
|
|
||||||
|
UI layout:
|
||||||
|
|
||||||
|
- Left sidebar: guild, voice channel, text channel/thread, connection state.
|
||||||
|
- Main area: message feed with filters and load-more cursor pagination.
|
||||||
|
- Right panel: review queue for `warn`, `flagged`, and `error` messages.
|
||||||
|
- Detail drawer/modal: message metadata, attachments, AI rationale, raw flags, reanalyze action.
|
||||||
|
|
||||||
|
Voice features stay functionally equivalent. Audio capture/playback code can be moved into React hooks but should not be behaviorally rewritten unless needed.
|
||||||
|
|
||||||
|
Build integration:
|
||||||
|
|
||||||
|
- Add Vite dev/build scripts.
|
||||||
|
- Express serves the built app from a stable public directory in production.
|
||||||
|
- During development, either run Vite separately or proxy API/WS to Express.
|
||||||
|
|
||||||
|
## Performance Rules
|
||||||
|
|
||||||
|
- Message capture must not wait on AI.
|
||||||
|
- Read APIs use cursor pagination and indexes.
|
||||||
|
- AI batches are bounded by token estimate and message count.
|
||||||
|
- UI fetches initial pages, then patches via WebSocket.
|
||||||
|
- Backlog sync should not block dashboard interactions.
|
||||||
|
- Avoid storing full raw LLM response per message when a batch table is available.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Capture errors log and do not crash Discord client event handlers.
|
||||||
|
- AI request failures mark target messages `error` with a short reason.
|
||||||
|
- Invalid LLM JSON triggers retry/split before marking errors.
|
||||||
|
- API validation returns 400 with structured error code.
|
||||||
|
- WebSocket reconnect logic stays client-side.
|
||||||
|
- Manual reanalysis provides recovery for bad AI results.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Backend:
|
||||||
|
|
||||||
|
- Unit tests for conversation context selection.
|
||||||
|
- Unit tests for LLM response parser and validation.
|
||||||
|
- Unit tests for queue batching/debounce behavior.
|
||||||
|
- Integration tests for message cursor pagination and review filters.
|
||||||
|
- Existing voice tests remain unchanged.
|
||||||
|
|
||||||
|
Frontend:
|
||||||
|
|
||||||
|
- Typecheck and Vite build.
|
||||||
|
- Component-level smoke tests may be added if test tooling is already practical.
|
||||||
|
- Manual browser verification: channel select, message feed, review panel, WebSocket updates, reanalyze action, voice controls.
|
||||||
|
|
||||||
|
## Implementation Scope
|
||||||
|
|
||||||
|
This is a full redesign of the message/AI/dashboard path. Voice recording and live audio behavior should be preserved unless a change is required to integrate the React dashboard.
|
||||||
|
|
||||||
|
Implementation should proceed incrementally:
|
||||||
|
|
||||||
|
1. Backend boundaries and typed broadcaster.
|
||||||
|
2. Store/query improvements and indexes.
|
||||||
|
3. Analysis queue/context/client rewrite.
|
||||||
|
4. Split API routes.
|
||||||
|
5. React/Vite dashboard.
|
||||||
|
6. Verification and cleanup of old static dashboard code.
|
||||||
@@ -28,7 +28,7 @@ export async function runMigrations(): Promise<void> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
{ error: error instanceof Error ? error.message : String(error) },
|
{ error: error instanceof Error ? error.message : String(error) },
|
||||||
"Migration failed"
|
"Migration failed",
|
||||||
);
|
);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ async function processBatch(
|
|||||||
|
|
||||||
// Broadcast analyzed messages
|
// Broadcast analyzed messages
|
||||||
for (const row of analyzedRows) {
|
for (const row of analyzedRows) {
|
||||||
(globalThis as any).broadcastMessageAnalyzed?.(row);
|
(globalThis as any).moderationBroadcaster?.messageAnalyzed(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear error cooldown on success
|
// Clear error cooldown on success
|
||||||
@@ -147,7 +147,7 @@ async function processBatch(
|
|||||||
error: lastError,
|
error: lastError,
|
||||||
});
|
});
|
||||||
if (row) {
|
if (row) {
|
||||||
(globalThis as any).broadcastMessageAnalyzed?.(row);
|
(globalThis as any).moderationBroadcaster?.messageAnalyzed(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,13 @@ export async function updateMessageAsEdited(
|
|||||||
edited_content: editedContent,
|
edited_content: editedContent,
|
||||||
edited_at: editedAt,
|
edited_at: editedAt,
|
||||||
type: "edited",
|
type: "edited",
|
||||||
|
ai_status: "pending",
|
||||||
|
ai_moderation_flags: null,
|
||||||
|
ai_moderation_score: null,
|
||||||
|
ai_moderation_raw: null,
|
||||||
|
ai_analysis: null,
|
||||||
|
ai_analyzed_at: null,
|
||||||
|
ai_error: null,
|
||||||
})
|
})
|
||||||
.where(eq(messagesTable.id, messageId));
|
.where(eq(messagesTable.id, messageId));
|
||||||
|
|
||||||
|
|||||||
@@ -220,26 +220,6 @@ export async function startWebserver(
|
|||||||
broadcaster.userState(users);
|
broadcaster.userState(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
(global as any).broadcastMessageCreated = (data: any) => {
|
|
||||||
broadcaster.messageCreated(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
(global as any).broadcastMessageUpdated = (data: any) => {
|
|
||||||
broadcaster.messageUpdated(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
(global as any).broadcastMessageDeleted = (data: any) => {
|
|
||||||
broadcaster.messageDeleted(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
(global as any).broadcastAttachmentUploaded = (data: any) => {
|
|
||||||
broadcaster.attachmentCreated(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
(global as any).broadcastMessageAnalyzed = (data: any) => {
|
|
||||||
broadcaster.messageAnalyzed(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Outbound: browser PCM (24kHz mono) → Opus → Discord ---
|
// --- Outbound: browser PCM (24kHz mono) → Opus → Discord ---
|
||||||
const RATE = 48000;
|
const RATE = 48000;
|
||||||
const CHANNELS = 2;
|
const CHANNELS = 2;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
insertMessage,
|
insertMessage,
|
||||||
listMessages,
|
listMessages,
|
||||||
listReviewMessages,
|
listReviewMessages,
|
||||||
|
updateMessageAsEdited,
|
||||||
|
getMessageById,
|
||||||
} from "../../src/moderation/messageStore";
|
} from "../../src/moderation/messageStore";
|
||||||
import {
|
import {
|
||||||
getDatabase,
|
getDatabase,
|
||||||
@@ -506,4 +508,67 @@ describe("message query integration tests", () => {
|
|||||||
expect(overlap).toHaveLength(0);
|
expect(overlap).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("updateMessageAsEdited", () => {
|
||||||
|
const createTestMessage = (
|
||||||
|
overrides: Partial<MessageRecord> = {},
|
||||||
|
): MessageRecord => ({
|
||||||
|
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||||
|
guild_id: "guild-123",
|
||||||
|
channel_id: "channel-456",
|
||||||
|
thread_id: null,
|
||||||
|
user_id: "user-789",
|
||||||
|
username: "testuser",
|
||||||
|
avatar_url: null,
|
||||||
|
content: "Test message",
|
||||||
|
edited_content: null,
|
||||||
|
created_at: Date.now(),
|
||||||
|
edited_at: null,
|
||||||
|
deleted_at: null,
|
||||||
|
type: "text",
|
||||||
|
metadata: null,
|
||||||
|
ai_status: "pending",
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets ai_status to pending and clears AI fields when message is edited", async () => {
|
||||||
|
const messageId = `msg-edit-test-${Date.now()}`;
|
||||||
|
const msg = createTestMessage({
|
||||||
|
id: messageId,
|
||||||
|
content: "original content",
|
||||||
|
ai_status: "clean",
|
||||||
|
ai_moderation_flags: "test_flag",
|
||||||
|
ai_moderation_score: 0.5,
|
||||||
|
ai_moderation_raw: '{"test": "data"}',
|
||||||
|
ai_analysis: "This is clean",
|
||||||
|
ai_analyzed_at: Date.now() - 10000,
|
||||||
|
ai_error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert message with AI analysis already done
|
||||||
|
await insertMessage(msg);
|
||||||
|
|
||||||
|
// Verify initial state
|
||||||
|
let retrieved = await getMessageById(messageId);
|
||||||
|
expect(retrieved?.ai_status).toBe("clean");
|
||||||
|
expect(retrieved?.ai_moderation_flags).toBe("test_flag");
|
||||||
|
expect(retrieved?.ai_moderation_score).toBe(0.5);
|
||||||
|
expect(retrieved?.ai_analysis).toBe("This is clean");
|
||||||
|
|
||||||
|
// Edit the message
|
||||||
|
await updateMessageAsEdited(messageId, "edited content", Date.now());
|
||||||
|
|
||||||
|
// Verify AI fields are reset
|
||||||
|
retrieved = await getMessageById(messageId);
|
||||||
|
expect(retrieved?.edited_content).toBe("edited content");
|
||||||
|
expect(retrieved?.type).toBe("edited");
|
||||||
|
expect(retrieved?.ai_status).toBe("pending");
|
||||||
|
expect(retrieved?.ai_moderation_flags).toBeNull();
|
||||||
|
expect(retrieved?.ai_moderation_score).toBeNull();
|
||||||
|
expect(retrieved?.ai_moderation_raw).toBeNull();
|
||||||
|
expect(retrieved?.ai_analysis).toBeNull();
|
||||||
|
expect(retrieved?.ai_analyzed_at).toBeNull();
|
||||||
|
expect(retrieved?.ai_error).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
|
import { config } from "dotenv";
|
||||||
|
|
||||||
|
config({ path: ".env.test" });
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
|
|||||||
Reference in New Issue
Block a user