fix: delete dashboard duplicates on create
This commit is contained in:
95
src/channels/discord-dashboard-create-cleanup.test.ts
Normal file
95
src/channels/discord-dashboard-create-cleanup.test.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('../config.js', () => ({
|
||||||
|
STATUS_CHANNEL_ID: 'status-channel',
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../logger.js', () => ({
|
||||||
|
logger: {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const readDashboardStatusMessageIdMock = vi.hoisted(() =>
|
||||||
|
vi.fn(() => 'dashboard-current'),
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.mock('../status-dashboard.js', () => ({
|
||||||
|
readDashboardStatusMessageId: readDashboardStatusMessageIdMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
deleteOwnDashboardDuplicateOnCreate,
|
||||||
|
isDashboardTrackedSend,
|
||||||
|
} from './discord-dashboard-create-cleanup.js';
|
||||||
|
|
||||||
|
function makeDashboardMessage(overrides: {
|
||||||
|
id?: string;
|
||||||
|
channelId?: string;
|
||||||
|
content?: string;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
id: overrides.id ?? 'dashboard-stale',
|
||||||
|
channelId: overrides.channelId ?? 'status-channel',
|
||||||
|
content: overrides.content ?? '🤖 *모델 구성*\nMemory 3.3/91.4GB',
|
||||||
|
delete: vi.fn().mockResolvedValue(undefined),
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('discord dashboard create cleanup', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
readDashboardStatusMessageIdMock.mockReturnValue('dashboard-current');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects tracked dashboard sends', () => {
|
||||||
|
expect(
|
||||||
|
isDashboardTrackedSend(
|
||||||
|
'dc:status-channel',
|
||||||
|
'🤖 *모델 구성*\nMemory 7.5/251.7GB',
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(isDashboardTrackedSend('dc:other', '🤖 *모델 구성*')).toBe(false);
|
||||||
|
expect(isDashboardTrackedSend('dc:status-channel', 'normal')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes stale own dashboard messages on create event', async () => {
|
||||||
|
const msg = makeDashboardMessage({ id: 'dashboard-stale' });
|
||||||
|
|
||||||
|
await deleteOwnDashboardDuplicateOnCreate({
|
||||||
|
message: msg,
|
||||||
|
channelName: 'discord',
|
||||||
|
graceUntil: 0,
|
||||||
|
now: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(msg.delete).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the tracked dashboard message', async () => {
|
||||||
|
const msg = makeDashboardMessage({ id: 'dashboard-current' });
|
||||||
|
|
||||||
|
await deleteOwnDashboardDuplicateOnCreate({
|
||||||
|
message: msg,
|
||||||
|
channelName: 'discord',
|
||||||
|
graceUntil: 0,
|
||||||
|
now: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(msg.delete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps dashboard messages during tracked send grace period', async () => {
|
||||||
|
const msg = makeDashboardMessage({ id: 'dashboard-new' });
|
||||||
|
|
||||||
|
await deleteOwnDashboardDuplicateOnCreate({
|
||||||
|
message: msg,
|
||||||
|
channelName: 'discord',
|
||||||
|
graceUntil: 200,
|
||||||
|
now: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(msg.delete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
55
src/channels/discord-dashboard-create-cleanup.ts
Normal file
55
src/channels/discord-dashboard-create-cleanup.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { Message } from 'discord.js';
|
||||||
|
|
||||||
|
import { STATUS_CHANNEL_ID } from '../config.js';
|
||||||
|
import { DASHBOARD_STATUS_MESSAGE_MARKER } from '../dashboard-message-cleanup.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
import { readDashboardStatusMessageId } from '../status-dashboard.js';
|
||||||
|
|
||||||
|
export const DASHBOARD_TRACKED_SEND_GRACE_MS = 5000;
|
||||||
|
|
||||||
|
export function isDashboardTrackedSend(jid: string, text: string): boolean {
|
||||||
|
return (
|
||||||
|
Boolean(STATUS_CHANNEL_ID) &&
|
||||||
|
jid.replace(/^dc:/, '') === STATUS_CHANNEL_ID &&
|
||||||
|
text.includes(DASHBOARD_STATUS_MESSAGE_MARKER)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteOwnDashboardDuplicateOnCreate(args: {
|
||||||
|
message: Message;
|
||||||
|
channelName: string;
|
||||||
|
graceUntil: number;
|
||||||
|
now?: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { message } = args;
|
||||||
|
if (!STATUS_CHANNEL_ID || message.channelId !== STATUS_CHANNEL_ID) return;
|
||||||
|
if (!message.content.includes(DASHBOARD_STATUS_MESSAGE_MARKER)) return;
|
||||||
|
|
||||||
|
const keepMessageId = readDashboardStatusMessageId(STATUS_CHANNEL_ID);
|
||||||
|
if (!keepMessageId || message.id === keepMessageId) return;
|
||||||
|
if ((args.now ?? Date.now()) < args.graceUntil) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await message.delete();
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
jid: `dc:${message.channelId}`,
|
||||||
|
messageId: message.id,
|
||||||
|
keepMessageId,
|
||||||
|
channelName: args.channelName,
|
||||||
|
},
|
||||||
|
'Deleted duplicate dashboard message on Discord create event',
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
{
|
||||||
|
jid: `dc:${message.channelId}`,
|
||||||
|
messageId: message.id,
|
||||||
|
keepMessageId,
|
||||||
|
channelName: args.channelName,
|
||||||
|
err,
|
||||||
|
},
|
||||||
|
'Failed to delete duplicate dashboard message on Discord create event',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ vi.mock('../config.js', () => ({
|
|||||||
TRIGGER_PATTERN: /^@Andy\b/i,
|
TRIGGER_PATTERN: /^@Andy\b/i,
|
||||||
DATA_DIR: '/tmp/ejclaw-test-data',
|
DATA_DIR: '/tmp/ejclaw-test-data',
|
||||||
CACHE_DIR: '/tmp/ejclaw-test-cache',
|
CACHE_DIR: '/tmp/ejclaw-test-cache',
|
||||||
|
STATUS_CHANNEL_ID: 'status-channel',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock logger
|
// Mock logger
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ import type {
|
|||||||
SendMessageOptions,
|
SendMessageOptions,
|
||||||
SendMessageResult,
|
SendMessageResult,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
|
import {
|
||||||
|
DASHBOARD_TRACKED_SEND_GRACE_MS,
|
||||||
|
deleteOwnDashboardDuplicateOnCreate,
|
||||||
|
isDashboardTrackedSend,
|
||||||
|
} from './discord-dashboard-create-cleanup.js';
|
||||||
import { deleteRecentDiscordMessagesByContent } from './discord-message-cleanup.js';
|
import { deleteRecentDiscordMessagesByContent } from './discord-message-cleanup.js';
|
||||||
import { prepareDiscordOutbound } from './discord-outbound.js';
|
import { prepareDiscordOutbound } from './discord-outbound.js';
|
||||||
|
|
||||||
@@ -193,6 +198,7 @@ export class DiscordChannel implements Channel {
|
|||||||
private agentTypeFilter?: AgentType;
|
private agentTypeFilter?: AgentType;
|
||||||
private receivesInbound: boolean;
|
private receivesInbound: boolean;
|
||||||
private ownsDiscordJids: boolean;
|
private ownsDiscordJids: boolean;
|
||||||
|
private dashboardTrackedSendGraceUntil = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
botToken: string,
|
botToken: string,
|
||||||
@@ -253,9 +259,16 @@ export class DiscordChannel implements Channel {
|
|||||||
private async handleMessageCreate(message: Message): Promise<void> {
|
private async handleMessageCreate(message: Message): Promise<void> {
|
||||||
const channelId = message.channelId;
|
const channelId = message.channelId;
|
||||||
const chatJid = `dc:${channelId}`;
|
const chatJid = `dc:${channelId}`;
|
||||||
if (!this.receivesInbound) return;
|
|
||||||
const isOwnBotMessage = message.author.id === this.client?.user?.id;
|
const isOwnBotMessage = message.author.id === this.client?.user?.id;
|
||||||
if (isOwnBotMessage) return;
|
if (isOwnBotMessage) {
|
||||||
|
await deleteOwnDashboardDuplicateOnCreate({
|
||||||
|
message,
|
||||||
|
channelName: this.name,
|
||||||
|
graceUntil: this.dashboardTrackedSendGraceUntil,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.receivesInbound) return;
|
||||||
if (message.author.bot && !hasReviewerLease(chatJid)) return;
|
if (message.author.bot && !hasReviewerLease(chatJid)) return;
|
||||||
|
|
||||||
let content = message.content;
|
let content = message.content;
|
||||||
@@ -635,6 +648,10 @@ export class DiscordChannel implements Channel {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const channelId = jid.replace(/^dc:/, '');
|
const channelId = jid.replace(/^dc:/, '');
|
||||||
|
if (isDashboardTrackedSend(jid, text)) {
|
||||||
|
this.dashboardTrackedSendGraceUntil =
|
||||||
|
Date.now() + DASHBOARD_TRACKED_SEND_GRACE_MS;
|
||||||
|
}
|
||||||
const channel = await this.client.channels.fetch(channelId);
|
const channel = await this.client.channels.fetch(channelId);
|
||||||
if (!channel || !('send' in channel)) {
|
if (!channel || !('send' in channel)) {
|
||||||
throw new Error(`Discord channel not found or not text-based: ${jid}`);
|
throw new Error(`Discord channel not found or not text-based: ${jid}`);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import type { Channel } from './types.js';
|
import type { Channel } from './types.js';
|
||||||
|
|
||||||
const DASHBOARD_STATUS_MESSAGE_MARKER = '🤖 *모델 구성*';
|
export const DASHBOARD_STATUS_MESSAGE_MARKER = '🤖 *모델 구성*';
|
||||||
const DASHBOARD_DUPLICATE_CLEANUP_LIMIT = 100;
|
const DASHBOARD_DUPLICATE_CLEANUP_LIMIT = 100;
|
||||||
|
|
||||||
function findDashboardCleanupChannel(opts: {
|
function findDashboardCleanupChannel(opts: {
|
||||||
|
|||||||
Reference in New Issue
Block a user