Files
intelligence-terminal/test/telegram-chat.test.mjs
MrSphay 1178146d59
All checks were successful
Codex Template Compliance / template-compliance (pull_request) Successful in 32s
Build / test-and-image (pull_request) Successful in 2m4s
fix: make dave telegram alerts conversational
2026-07-11 10:58:37 +02:00

227 lines
9.2 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { TelegramAlerter } from '../lib/alerts/telegram.mjs';
import { TelegramChatAssistant, buildTelegramChatContext } from '../lib/llm/telegram-chat.mjs';
import { extractBotChannelMessages } from '../apis/sources/telegram.mjs';
test('Telegram AI chat uses bounded history and current intelligence context', async () => {
const calls = [];
const provider = {
isConfigured: true,
async complete(systemPrompt, userMessage, options) {
calls.push({ systemPrompt, userMessage, options });
return { text: calls.length === 1 ? 'First answer' : 'Second answer' };
},
};
const assistant = new TelegramChatAssistant({
provider,
getContext: () => '{"direction":"risk-off"}',
historyMessages: 4,
maxInputChars: 200,
maxTokens: 1024,
timeoutMs: 120000,
});
assert.equal(await assistant.reply('What changed today?', { chatId: 42 }), 'First answer');
assert.equal(await assistant.reply('Explain the implications in detail', { chatId: 42 }), 'Second answer');
assert.match(calls[0].systemPrompt, /untrusted evidence/i);
assert.match(calls[0].systemPrompt, /Your name is DAVE/);
assert.match(calls[0].systemPrompt, /ADAPTIVE WRITING STYLE/);
assert.match(calls[0].userMessage, /risk-off/);
assert.deepEqual(calls[0].options, { maxTokens: 1024, timeout: 120000 });
assert.match(calls[1].userMessage, /User: What changed today\?/);
assert.match(calls[1].userMessage, /Assistant: First answer/);
assert.match(calls[1].userMessage, /NEW USER MESSAGE: Explain the implications in detail/);
assert.equal(assistant.historySize(42), 4);
assistant.reset(42);
assert.equal(assistant.historySize(42), 0);
});
test('Telegram AI chat reports missing LLM configuration', async () => {
const assistant = new TelegramChatAssistant({ provider: null });
assert.match(await assistant.reply('hello', { chatId: 1 }), /unavailable/i);
});
test('Telegram AI chat enforces the encrypted profile language in provider prompts', async () => {
const calls = [];
const assistant = new TelegramChatAssistant({
provider: {
isConfigured: true,
async complete(systemPrompt, userMessage) {
calls.push({ systemPrompt, userMessage });
return { text: 'Antwort auf Deutsch.' };
},
},
getPreferredLanguage: () => 'de',
});
assert.equal(await assistant.reply('Kurzer Lagecheck', { chatId: 42 }), 'Antwort auf Deutsch.');
assert.match(calls[0].systemPrompt, /REQUIRED RESPONSE LANGUAGE: German \(de\)/);
assert.match(calls[0].userMessage, /All user-visible prose in the final answer must be German/);
});
test('Telegram chat context is compact and operationally useful', () => {
const context = JSON.parse(buildTelegramChatContext({
meta: { generatedAt: '2026-07-05T10:00:00Z' },
delta: { summary: { direction: 'risk-off', totalChanges: 3, criticalChanges: 1 } },
ideas: [{ title: 'Gold hedge', type: 'HEDGE', ticker: 'GLD', confidence: 'HIGH' }],
news: [{ title: 'Headline', source: 'Feed', url: 'https://example.test/story' }],
sourceHealth: [{ name: 'ACLED', status: 'degraded', error: 'missing credentials' }],
}, { status: 'degraded', sourcesOk: 22, sourcesDegraded: 1 }));
assert.equal(context.direction, 'risk-off');
assert.equal(context.ideas[0].ticker, 'GLD');
assert.equal(context.news[0].url, 'https://example.test/story');
assert.equal(context.degradedSources[0].name, 'ACLED');
});
test('Telegram transport routes authorized free text as plain-text AI reply', async () => {
const alerter = new TelegramAlerter({ botToken: 'test-token', chatId: '42' });
let handled = 0;
const requests = [];
alerter.onMessage(async (text) => {
handled++;
return { text: `Answer: ${text}`, parseMode: null };
});
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, options = {}) => {
requests.push({ url, body: options.body ? JSON.parse(options.body) : null });
if (url.includes('/getUpdates')) {
return {
ok: true,
json: async () => ({
ok: true,
result: [
{ update_id: 1, message: { message_id: 10, text: 'ignore me', chat: { id: 99 } } },
{ update_id: 2, message: { message_id: 11, text: 'What changed?', chat: { id: 42 } } },
],
}),
};
}
return { ok: true, json: async () => ({ ok: true, result: { message_id: 12 } }) };
};
try {
await alerter._pollUpdates();
} finally {
globalThis.fetch = originalFetch;
}
assert.equal(handled, 1);
const sent = requests.find(request => request.url.includes('/sendMessage'));
assert.equal(sent.body.text, 'Answer: What changed?');
assert.equal('parse_mode' in sent.body, false);
assert.equal(sent.body.reply_to_message_id, 11);
});
test('Telegram conversation delivery strips visible markdown and splits large AI replies', async () => {
const alerter = new TelegramAlerter({ botToken: 'test-token', chatId: '42' });
const requests = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, options = {}) => {
requests.push({ url, body: options.body ? JSON.parse(options.body) : null });
return { ok: true, json: async () => ({ ok: true, result: { message_id: requests.length } }) };
};
try {
const text = '**Aktuelle Lage:**\n\n' + [
'Erster kurzer Absatz mit einer direkten Einschaetzung.',
'Zweiter Absatz mit mehr Kontext und genug Laenge, damit er in einer eigenen Nachricht landet. '.repeat(6),
'Dritter Absatz mit einer Rueckfrage, auf die der Nutzer direkt antworten kann. '.repeat(5),
].join('\n\n');
const result = await alerter.sendConversation(text, { maxChunkChars: 300, delayMs: 0, replyToMessageId: 7 });
assert.equal(result.ok, true);
} finally {
globalThis.fetch = originalFetch;
}
const sent = requests.filter(request => request.url.includes('/sendMessage'));
assert.ok(sent.length >= 2);
assert.doesNotMatch(sent[0].body.text, /\*\*/);
assert.equal('parse_mode' in sent[0].body, false);
assert.equal(sent[0].body.reply_to_message_id, 7);
assert.equal('reply_to_message_id' in sent[1].body, false);
});
test('Telegram conversation defaults to short human-sized chat chunks', async () => {
const alerter = new TelegramAlerter({ botToken: 'test-token', chatId: '42' });
const requests = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, options = {}) => {
requests.push({ url, body: options.body ? JSON.parse(options.body) : null });
return { ok: true, json: async () => ({ ok: true, result: { message_id: requests.length } }) };
};
try {
const text = [
'Das ist die erste kurze Einschaetzung. Sie soll separat lesbar bleiben und nicht wie ein Bericht wirken.',
'Hier kommt mehr Kontext, aber auch dieser Kontext bleibt in natuerlichen Chat-Haeppchen. '.repeat(8),
'Soll ich die Details danach sauber aufdrillen?',
].join('\n\n');
const result = await alerter.sendConversation(text, { delayMs: 0 });
assert.equal(result.ok, true);
} finally {
globalThis.fetch = originalFetch;
}
const sent = requests.filter(request => request.url.includes('/sendMessage'));
assert.ok(sent.length >= 3);
for (const message of sent) {
assert.ok(message.body.text.length <= 420, `message too long: ${message.body.text.length}`);
assert.equal('parse_mode' in message.body, false);
}
});
test('Telegram tiered fallback alert is sent as conversational DAVE messages', async () => {
const alerter = new TelegramAlerter({ botToken: 'test-token', chatId: '42' });
const requests = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, options = {}) => {
requests.push({ url, body: options.body ? JSON.parse(options.body) : null });
return { ok: true, json: async () => ({ ok: true, result: { message_id: requests.length } }) };
};
const memory = {
getAlertedSignals: () => ({}),
markAsAlerted: () => {},
};
const delta = {
summary: { totalChanges: 1, criticalChanges: 1, direction: 'mixed' },
signals: {
new: [{
key: 'source_degradation',
severity: 'critical',
label: '7 additional sources failing',
direction: 'up',
}],
escalated: [],
},
};
try {
const sent = await alerter.evaluateAndAlert(null, delta, memory, { language: 'de', delayMs: 0 });
assert.equal(sent, true);
} finally {
globalThis.fetch = originalFetch;
}
const texts = requests.filter(request => request.url.includes('/sendMessage')).map(request => request.body.text);
assert.ok(texts.length >= 2);
assert.match(texts[0], /^\[DAVE \/\/ HINWEIS\]/);
assert.doesNotMatch(texts.join('\n'), /CRUCIX ROUTINE|Confidence:|Direction:|Cross-correlation:|Werkzeuge|Tools used/);
for (const text of texts) assert.ok(text.length <= 380, `alert chunk too long: ${text.length}`);
});
test('Telegram OSINT extraction excludes private AI chat messages', () => {
const messages = extractBotChannelMessages([
{ update_id: 1, message: { text: 'private question', chat: { id: 42, type: 'private' } } },
{ update_id: 2, channel_post: { text: 'public channel report', chat: { title: 'OSINT', type: 'channel' } } },
]);
assert.equal(messages.length, 1);
assert.equal(messages[0].text, 'public channel report');
assert.equal(messages[0].chat, 'OSINT');
});