import { test } from "bun:test"; import assert from "node:assert/strict"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { ClankerBot } from "../bot.ts"; import { rmTempDir } from "../testHelpers.ts"; import { buildReplyPipelineRuntime } from "./botRuntimeFactories.ts"; import { maybeReplyToMessagePipeline } from "./replyPipeline.ts"; import type { ActiveReply } from "../tools/activeReplyRegistry.ts"; import { ActiveReplyRegistry, buildTextReplyScopeKey } from "../tools/activeReplyRegistry.ts"; import { createAbortError } from "../tools/abortError.ts"; import { Store } from "../store/store.ts"; import { createTestSettingsPatch } from "../testSettings.ts"; import type { SubAgentSession } from "../agents/subAgentSession.ts";
class TrackingActiveReplyRegistry extends ActiveReplyRegistry { clearCalls = 0;
override clear(reply: ActiveReply | null | undefined) { this.clearCalls += 1; super.clear(reply); } }
async function withTempStore(run: (store: Store) => Promise) { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clanker-reply-pipeline-test-")); const dbPath = path.join(dir, "clanker.db"); const store = new Store(dbPath); store.init();
try { await run(store); } finally { store.close(); await rmTempDir(dir); } }
function applyBaselineSettings(store: Store, channelId: string) { store.patchSettings(createTestSettingsPatch({ identity: { botName: "clanky" }, interaction: { activity: { ambientReplyEagerness: 65, reactivity: 0, minSecondsBetweenMessages: 0, replyCoalesceWindowSeconds: 0, replyCoalesceMaxMessages: 1 } }, permissions: { replies: { allowReplies: true, allowUnsolicitedReplies: true, allowReactions: false, replyChannelIds: [], allowedChannelIds: [channelId], blockedChannelIds: [], blockedUserIds: [], maxMessagesPerHour: 120, maxReactionsPerHour: 0 } }, memory: { enabled: false, promptSlice: { maxRecentMessages: 12 } }, agentStack: { runtimeConfig: { research: { enabled: false, maxSearchesPerHour: 0 } } }, media: { videoContext: { enabled: false, maxLookupsPerHour: 0 }, vision: { enabled: false } }, initiative: { discovery: { allowReplyImages: false, allowReplyVideos: false, allowReplyGifs: false, sources: { reddit: false, hackerNews: false, youtube: false, rss: false, x: false } } } })); }
function buildGuild() { return { id: "guild-1", emojis: { cache: { map() { return []; } } }, members: { cache: new Map() } }; }
function buildChannel({
guild,
channelId,
channelSendPayloads,
typingCallsRef
}: {
guild: ReturnType;
channelId: string;
channelSendPayloads: Array<Record<string, unknown>>;
typingCallsRef: { count: number };
}) {
return {
id: channelId,
guildId: guild.id,
name: "general",
guild,
isTextBased() {
return true;
},
async sendTyping() {
typingCallsRef.count += 1;
},
async send(payload: Record<string, unknown>) {
channelSendPayloads.push(payload);
return {
id: standalone-${Date.now()},
createdTimestamp: Date.now(),
guildId: guild.id,
channelId,
content: String(payload.content || ""),
attachments: new Map(),
embeds: []
};
}
};
}
function buildIncomingMessage({
guild,
channel,
messageId,
content,
replyPayloads
}: {
guild: ReturnType;
channel: ReturnType;
messageId: string;
content: string;
replyPayloads: Array<Record<string, unknown>>;
}) {
return {
id: messageId,
createdTimestamp: Date.now(),
guildId: guild.id,
channelId: channel.id,
guild,
channel,
author: {
id: "user-1",
username: "alice",
bot: false
},
member: {
displayName: "alice"
},
content,
mentions: {
users: {
size: 0,
has() {
return false;
}
},
repliedUser: null
},
reference: null,
attachments: new Map(),
embeds: [],
reactions: {
cache: new Map()
},
async react() {
return undefined;
},
async reply(payload: Record<string, unknown>) {
replyPayloads.push(payload);
return {
id: reply-${Date.now()},
createdTimestamp: Date.now(),
guildId: guild.id,
channelId: channel.id,
content: String(payload.content || ""),
attachments: new Map(),
embeds: []
};
}
};
}
test("maybeReplyToMessagePipeline treats an aborted in-flight reply as handled and clears tracking", async () => { await withTempStore(async (store) => { const channelId = "chan-1"; applyBaselineSettings(store, channelId);
let resolveGenerateStarted: (() => void) | null = null;
const generateStarted = new Promise<void>((resolve) => {
resolveGenerateStarted = resolve;
});
const replyPayloads: Array<Record<string, unknown>> = [];
const channelSendPayloads: Array<Record<string, unknown>> = [];
const typingCallsRef = { count: 0 };
const bot = new ClankerBot({
appConfig: {},
store,
llm: {
async generate(payload) {
resolveGenerateStarted?.();
return await new Promise((_, reject) => {
const abortWithReason = () => {
reject(createAbortError(payload.signal?.reason || "Reply cancelled"));
};
if (payload.signal?.aborted) {
abortWithReason();
return;
}
payload.signal?.addEventListener("abort", abortWithReason, { once: true });
});
}
},
memory: null,
discovery: null,
search: null,
gifs: null,
video: null
});
const activeReplies = new TrackingActiveReplyRegistry();
bot.activeReplies = activeReplies;
bot.client.user = {
id: "bot-1",
username: "clanky",
tag: "clanky#0001"
};
const guild = buildGuild();
const channel = buildChannel({
guild,
channelId,
channelSendPayloads,
typingCallsRef
});
const message = buildIncomingMessage({
guild,
channel,
messageId: "msg-1",
content: "clanker can you answer this?",
replyPayloads
});
const settings = store.getSettings();
const runtime = buildReplyPipelineRuntime(bot, {
captionTimestamps: [],
unsolicitedReplyContextWindow: 2
});
const replyScopeKey = buildTextReplyScopeKey({
guildId: guild.id,
channelId
});
const pipelinePromise = maybeReplyToMessagePipeline(runtime, message, settings, {
source: "message_event",
forceDecisionLoop: true,
forceRespond: true,
recentMessages: [],
triggerMessageIds: [message.id],
addressSignal: {
direct: true,
inferred: false,
triggered: true,
reason: "direct_address"
}
});
assert.equal(activeReplies.has(replyScopeKey), true);
await generateStarted;
assert.equal(activeReplies.has(replyScopeKey), true);
const cancelledCount = activeReplies.abortAll(replyScopeKey, "User requested cancellation");
assert.equal(cancelledCount, 1);
const handled = await pipelinePromise;
assert.equal(handled, true);
assert.equal(activeReplies.has(replyScopeKey), false);
assert.equal(activeReplies.clearCalls, 1);
assert.equal(typingCallsRef.count, 1);
assert.equal(channelSendPayloads.length, 0);
assert.equal(replyPayloads.length, 0);
}); });
test("maybeReplyToMessagePipeline recovers unstructured model output as prose reply", async () => { await withTempStore(async (store) => { const channelId = "chan-1"; applyBaselineSettings(store, channelId);
const replyPayloads: Array<Record<string, unknown>> = [];
const channelSendPayloads: Array<Record<string, unknown>> = [];
const typingCallsRef = { count: 0 };
const bot = new ClankerBot({
appConfig: {},
store,
llm: {
async generate() {
return {
text: "I need to consider the context here before I answer.",
toolCalls: [],
rawContent: null,
provider: "claude-oauth",
model: "claude-opus-4-6",
usage: {
inputTokens: 10,
outputTokens: 8,
cacheWriteTokens: 0,
cacheReadTokens: 0
},
costUsd: 0
};
}
},
memory: null,
discovery: null,
search: null,
gifs: null,
video: null
});
bot.client.user = {
id: "bot-1",
username: "clanky",
tag: "clanky#0001"
};
const guild = buildGuild();
const channel = buildChannel({
guild,
channelId,
channelSendPayloads,
typingCallsRef
});
const message = buildIncomingMessage({
guild,
channel,
messageId: "msg-1",
content: "play daft punk",
replyPayloads
});
const settings = store.getSettings();
const runtime = buildReplyPipelineRuntime(bot, {
captionTimestamps: [],
unsolicitedReplyContextWindow: 2
});
const handled = await maybeReplyToMessagePipeline(runtime, message, settings, {
source: "text_thought_loop",
forceDecisionLoop: true,
recentMessages: [],
triggerMessageIds: [message.id],
addressSignal: {
direct: false,
inferred: false,
triggered: false,
reason: "llm_decides",
confidence: 0,
threshold: 0.62,
confidenceSource: "fallback"
}
});
assert.equal(handled, true);
assert.equal(typingCallsRef.count, 1);
const sentPayload = channelSendPayloads[0] || replyPayloads[0];
assert.ok(sentPayload, "expected a sent message (via channel.send or message.reply)");
assert.equal(
sentPayload?.content,
"I need to consider the context here before I answer."
);
const warning = store.getRecentActions(10).find((entry) => entry.kind === "bot_warning");
assert.equal(warning?.content, "structured_output_recovered_as_prose");
}); });
test("maybeReplyToMessagePipeline skips tool narration prose after a tool loop", async () => { await withTempStore(async (store) => { const channelId = "chan-1"; applyBaselineSettings(store, channelId);
store.recordMessage({
messageId: "history-1",
createdAt: Date.now() - 5_000,
guildId: "guild-1",
channelId,
authorId: "user-2",
authorName: "bob",
isBot: false,
content: "CURSED conk said hello earlier",
referencedMessageId: null
});
const replyPayloads: Array<Record<string, unknown>> = [];
const channelSendPayloads: Array<Record<string, unknown>> = [];
const typingCallsRef = { count: 0 };
let generateCount = 0;
const bot = new ClankerBot({
appConfig: {},
store,
llm: {
async generate() {
generateCount += 1;
if (generateCount === 1) {
return {
text: "",
rawContent: null,
toolCalls: [
{
id: "tool-1",
name: "conversation_search",
input: {
query: "what did cursed say"
}
}
],
provider: "claude-oauth",
model: "claude-opus-4-6",
usage: {
inputTokens: 10,
outputTokens: 8,
cacheWriteTokens: 0,
cacheReadTokens: 0
},
costUsd: 0
};
}
return {
text: "I searched the conversation history and found that cursed conk said hello earlier today.",
rawContent: null,
toolCalls: [],
provider: "claude-oauth",
model: "claude-opus-4-6",
usage: {
inputTokens: 10,
outputTokens: 8,
cacheWriteTokens: 0,
cacheReadTokens: 0
},
costUsd: 0
};
}
},
memory: null,
discovery: null,
search: null,
gifs: null,
video: null
});
bot.client.user = {
id: "bot-1",
username: "clanky",
tag: "clanky#0001"
};
const guild = buildGuild();
const channel = buildChannel({
guild,
channelId,
channelSendPayloads,
typingCallsRef
});
const message = buildIncomingMessage({
guild,
channel,
messageId: "msg-1",
content: "what did cursed say",
replyPayloads
});
const settings = store.getSettings();
const runtime = buildReplyPipelineRuntime(bot, {
captionTimestamps: [],
unsolicitedReplyContextWindow: 2
});
const handled = await maybeReplyToMessagePipeline(runtime, message, settings, {
source: "text_thought_loop",
forceDecisionLoop: true,
recentMessages: [],
triggerMessageIds: [message.id],
addressSignal: {
direct: true,
inferred: false,
triggered: true,
reason: "direct",
confidence: 1,
threshold: 0.62,
confidenceSource: "direct"
}
});
assert.equal(handled, false);
assert.equal(replyPayloads.length, 0);
assert.equal(channelSendPayloads.length, 0);
assert.equal(
store.getRecentActions(20).some((entry) => entry.kind === "reply_skipped" && entry.content === "invalid_structured_output_after_tool_loop"),
true
);
}); });
test("maybeReplyToMessagePipeline lets the model attach tool-returned images in the final reply", async () => { await withTempStore(async (store) => { const channelId = "chan-1"; applyBaselineSettings(store, channelId);
const llmCalls: Array<{
userPrompt: string;
imageInputs?: unknown;
contextMessages?: unknown;
}> = [];
const replyPayloads: Array<Record<string, unknown>> = [];
const channelSendPayloads: Array<Record<string, unknown>> = [];
const typingCallsRef = { count: 0 };
let generateCount = 0;
const bot = new ClankerBot({
appConfig: {},
store,
llm: {
async generate(payload) {
llmCalls.push({
userPrompt: String(payload.userPrompt || ""),
imageInputs: payload.imageInputs,
contextMessages: payload.contextMessages
});
generateCount += 1;
if (generateCount === 1) {
return {
text: "let me check",
toolCalls: [
{
id: "tool-1",
name: "browser_browse",
input: {
query: "show me the stream"
}
}
],
rawContent: null,
provider: "claude-oauth",
model: "claude-opus-4-6",
usage: {
inputTokens: 12,
outputTokens: 10,
cacheWriteTokens: 0,
cacheReadTokens: 0
},
costUsd: 0
};
}
return {
text: JSON.stringify({
text: "yep here it is",
skip: false,
reactionEmoji: null,
media: { type: "tool_images", prompt: null },
automationAction: {
operation: "none",
title: null,
instruction: null,
schedule: null,
targetQuery: null,
automationId: null,
runImmediately: false,
targetChannelId: null
},
screenWatchIntent: {
action: "none",
confidence: 0,
reason: null
},
}),
toolCalls: [],
rawContent: null,
provider: "claude-oauth",
model: "claude-opus-4-6",
usage: {
inputTokens: 14,
outputTokens: 18,
cacheWriteTokens: 0,
cacheReadTokens: 0
},
costUsd: 0
};
}
},
memory: null,
discovery: null,
search: null,
gifs: null,
video: null
});
bot.client.user = {
id: "bot-1",
username: "clanky",
tag: "clanky#0001"
};
const guild = buildGuild();
const channel = buildChannel({
guild,
channelId,
channelSendPayloads,
typingCallsRef
});
const message = buildIncomingMessage({
guild,
channel,
messageId: "msg-1",
content: "can you show me the screenshot?",
replyPayloads
});
const settings = store.getSettings();
const runtime = buildReplyPipelineRuntime(bot, {
captionTimestamps: [],
unsolicitedReplyContextWindow: 2
});
runtime.buildBrowserBrowseContext = () => ({
requested: false,
configured: true,
enabled: true,
used: false,
blockedByBudget: false,
error: null,
query: "",
text: "",
imageInputs: [],
steps: 0,
hitStepLimit: false,
budget: {
maxPerHour: 10,
used: 0,
remaining: 10,
canBrowse: true
}
});
runtime.runModelRequestedBrowserBrowse = async () => ({
used: true,
text: "Browser screenshot captured.",
imageInputs: [
{
mediaType: "image/png",
dataBase64: Buffer.from("browser-shot").toString("base64")
}
],
steps: 1,
hitStepLimit: false,
error: null,
blockedByBudget: false
});
const handled = await maybeReplyToMessagePipeline(runtime, message, settings, {
source: "message_event",
forceDecisionLoop: true,
forceRespond: true,
recentMessages: [],
triggerMessageIds: [message.id],
addressSignal: {
direct: true,
inferred: false,
triggered: true,
reason: "direct_address"
}
});
assert.equal(handled, true);
assert.equal(typingCallsRef.count, 1);
const sentPayload = replyPayloads[0] || channelSendPayloads[0];
assert.ok(sentPayload, "expected a sent reply payload");
assert.equal(sentPayload.content, "yep here it is");
assert.equal(Array.isArray(sentPayload.files), true);
assert.equal(sentPayload.files?.length, 1);
assert.equal(sentPayload.files?.[0]?.name, "clanky-tool-1.png");
assert.match(llmCalls[1]?.userPrompt || "", /tool_images/);
}); });
test("maybeReplyToMessagePipeline includes current message video attachments with VID refs in the prompt", async () => { await withTempStore(async (store) => { const channelId = "chan-1"; applyBaselineSettings(store, channelId);
const llmCalls: Array<{ userPrompt: string }> = [];
const replyPayloads: Array<Record<string, unknown>> = [];
const channelSendPayloads: Array<Record<string, unknown>> = [];
const typingCallsRef = { count: 0 };
const bot = new ClankerBot({
appConfig: {},
store,
llm: {
async generate(payload) {
llmCalls.push({ userPrompt: String(payload.userPrompt || "") });
return {
text: JSON.stringify({
text: "I can check that clip if needed.",
skip: false,
reactionEmoji: null,
media: null,
automationAction: {
operation: "none",
title: null,
instruction: null,
schedule: null,
targetQuery: null,
automationId: null,
runImmediately: false,
targetChannelId: null
},
screenWatchIntent: {
action: "none",
confidence: 0,
reason: null
}
}),
toolCalls: [],
rawContent: null,
provider: "claude-oauth",
model: "claude-opus-4-6",
usage: {
inputTokens: 10,
outputTokens: 10,
cacheWriteTokens: 0,
cacheReadTokens: 0
},
costUsd: 0
};
}
},
memory: null,
discovery: null,
search: null,
gifs: null,
video: null
});
bot.client.user = {
id: "bot-1",
username: "clanky",
tag: "clanky#0001"
};
const guild = buildGuild();
const channel = buildChannel({
guild,
channelId,
channelSendPayloads,
typingCallsRef
});
const message = buildIncomingMessage({
guild,
channel,
messageId: "msg-video-1",
content: "can you break down this clip?",
replyPayloads
}) as Record<string, unknown>;
message.attachments = new Map([
[
"video-1",
{
url: "https://cdn.discordapp.com/attachments/1/2/demo.mp4",
proxyURL: "https://media.discordapp.net/attachments/1/2/demo.mp4",
name: "demo.mp4",
contentType: "video/mp4"
}
]
]);
const settings = store.getSettings();
const runtime = buildReplyPipelineRuntime(bot, {
captionTimestamps: [],
unsolicitedReplyContextWindow: 2
});
const handled = await maybeReplyToMessagePipeline(runtime, message as never, settings, {
source: "message_event",
forceDecisionLoop: true,
forceRespond: true,
recentMessages: [],
triggerMessageIds: [String(message.id || "")],
addressSignal: {
direct: true,
inferred: false,
triggered: true,
reason: "direct_address"
}
});
assert.equal(handled, true);
assert.equal(typingCallsRef.count, 1);
const sentPayload = replyPayloads[0] || channelSendPayloads[0];
assert.ok(sentPayload, "expected a sent reply payload");
assert.equal(sentPayload.content, "I can check that clip if needed.");
assert.match(llmCalls[0]?.userPrompt || "", /Current turn video\/GIF refs:/);
assert.match(llmCalls[0]?.userPrompt || "", /VID 1: demo\.mp4 \(video\/mp4\)/);
assert.match(
llmCalls[0]?.userPrompt || "",
/https:\/\/cdn\.discordapp\.com\/attachments\/1\/2\/demo\.mp4/
);
}); });
test("maybeReplyToMessagePipeline inspects coalesced GIF video links before answering visual questions", async () => { await withTempStore(async (store) => { const channelId = "chan-1"; applyBaselineSettings(store, channelId); store.patchSettings({ media: { videoContext: { enabled: true, maxLookupsPerHour: 12, maxVideosPerMessage: 2, keyframeIntervalSeconds: 1, maxKeyframesPerVideo: 2 } } });
const llmCalls: Array<{ userPrompt: string; imageInputs: Array<Record<string, unknown>> }> = [];
const replyPayloads: Array<Record<string, unknown>> = [];
const channelSendPayloads: Array<Record<string, unknown>> = [];
const typingCallsRef = { count: 0 };
const fetchedTargets: string[] = [];
const video = {
extractVideoTargets(url: string) {
return [
{
key: `direct:${url}`,
url,
kind: "direct",
videoId: null,
forceDirect: true
}
];
},
async fetchContexts(payload: { targets: Array<{ url: string }> }) {
fetchedTargets.push(...payload.targets.map((target) => String(target.url || "")));
return {
videos: payload.targets.map((target) => ({
provider: "direct",
title: "wow taiko",
url: target.url,
frameImages: [
{
filename: "frame-001.jpg",
contentType: "image/jpeg",
mediaType: "image/jpeg",
dataBase64: "ZnJhbWU="
}
],
keyframeCount: 1,
keyframeError: null,
transcript: ""
})),
errors: []
};
}
};
const bot = new ClankerBot({
appConfig: {},
store,
llm: {
async generate(payload) {
llmCalls.push({
userPrompt: String(payload.userPrompt || ""),
imageInputs: Array.isArray(payload.imageInputs) ? payload.imageInputs : []
});
return {
text: JSON.stringify({
text: "it is spinning and licking the air",
skip: false,
reactionEmoji: null,
media: null,
automationAction: {
operation: "none",
title: null,
instruction: null,
schedule: null,
targetQuery: null,
automationId: null,
runImmediately: false,
targetChannelId: null
},
screenWatchIntent: {
action: "none",
confidence: 0,
reason: null
}
}),
toolCalls: [],
rawContent: null,
provider: "claude-oauth",
model: "claude-opus-4-6",
usage: {
inputTokens: 10,
outputTokens: 10,
cacheWriteTokens: 0,
cacheReadTokens: 0
},
costUsd: 0
};
}
},
memory: null,
discovery: null,
search: null,
gifs: null,
video
});
bot.client.user = {
id: "bot-1",
username: "clanky",
tag: "clanky#0001"
};
const guild = buildGuild();
const channel = buildChannel({
guild,
channelId,
channelSendPayloads,
typingCallsRef
});
const message = buildIncomingMessage({
guild,
channel,
messageId: "msg-gif-question",
content: "clanky, tell me what is the main point of this gif",
replyPayloads
});
const settings = store.getSettings();
const runtime = buildReplyPipelineRuntime(bot, {
captionTimestamps: [],
unsolicitedReplyContextWindow: 2
});
const recentMessages = [
{
message_id: "msg-gif-link",
author_name: "alice",
content: "https://tenor.com/view/wow-taiko-spin-cute-lick-gif-25795014 https://media.tenor.com/rAWXcy9O6XUAAAPo/wow-taiko.mp4",
created_at: new Date().toISOString(),
is_bot: false
},
{
message_id: "msg-gif-question",
author_name: "alice",
content: "clanky, tell me what is the main point of this gif",
created_at: new Date().toISOString(),
is_bot: false
}
];
const handled = await maybeReplyToMessagePipeline(runtime, message as never, settings, {
source: "message_event_coalesced",
forceDecisionLoop: true,
forceRespond: true,
recentMessages,
triggerMessageIds: ["msg-gif-link", String(message.id || "")],
addressSignal: {
direct: true,
inferred: true,
triggered: true,
reason: "name_exact"
}
});
assert.equal(handled, true);
assert.deepEqual(fetchedTargets, ["https://media.tenor.com/rAWXcy9O6XUAAAPo/wow-taiko.mp4"]);
assert.equal(llmCalls[0]?.imageInputs.length, 1);
assert.match(llmCalls[0]?.userPrompt || "", /Current turn video\/GIF refs:/);
assert.match(llmCalls[0]?.userPrompt || "", /VID 1: wow-taiko\.mp4 \(video\/mp4\)/);
assert.match(llmCalls[0]?.userPrompt || "", /VISUAL MEDIA CONTEXT/);
assert.match(llmCalls[0]?.userPrompt || "", /keyframes attached: 1/);
}); });
test("maybeReplyToMessagePipeline calls out missing video dependencies when GIF frames cannot be sampled", async () => { await withTempStore(async (store) => { const channelId = "chan-1"; applyBaselineSettings(store, channelId); store.patchSettings({ media: { videoContext: { enabled: true, maxLookupsPerHour: 12, maxVideosPerMessage: 2, keyframeIntervalSeconds: 1, maxKeyframesPerVideo: 2 } } });
const llmCalls: Array<{ userPrompt: string; imageInputs: Array<Record<string, unknown>> }> = [];
const replyPayloads: Array<Record<string, unknown>> = [];
const channelSendPayloads: Array<Record<string, unknown>> = [];
const typingCallsRef = { count: 0 };
const video = {
extractVideoTargets(url: string) {
return [
{
key: `direct:${url}`,
url,
kind: "direct",
videoId: null,
forceDirect: true
}
];
},
async fetchContexts(payload: { targets: Array<{ url: string }> }) {
return {
videos: payload.targets.map((target) => ({
provider: "direct",
title: "wow taiko",
url: target.url,
frameImages: [],
keyframeCount: 0,
keyframeError: "Local runtime dependency missing: ffmpeg is required to sample frames from GIF/video media. Install ffmpeg and restart the bot.",
keyframeErrorCode: "missing_ffmpeg",
missingDependencies: ["ffmpeg"],
transcript: ""
})),
errors: []
};
}
};
const bot = new ClankerBot({
appConfig: {},
store,
llm: {
async generate(payload) {
llmCalls.push({
userPrompt: String(payload.userPrompt || ""),
imageInputs: Array.isArray(payload.imageInputs) ? payload.imageInputs : []
});
return {
text: JSON.stringify({
text: "I cannot inspect the pixels until ffmpeg is installed.",
skip: false,
reactionEmoji: null,
media: null,
automationAction: {
operation: "none",
title: null,
instruction: null,
schedule: null,
targetQuery: null,
automationId: null,
runImmediately: false,
targetChannelId: null
},
screenWatchIntent: {
action: "none",
confidence: 0,
reason: null
}
}),
toolCalls: [],
rawContent: null,
provider: "claude-oauth",
model: "claude-opus-4-6",
usage: {
inputTokens: 10,
outputTokens: 10,
cacheWriteTokens: 0,
cacheReadTokens: 0
},
costUsd: 0
};
}
},
memory: null,
discovery: null,
search: null,
gifs: null,
video
});
bot.client.user = {
id: "bot-1",
username: "clanky",
tag: "clanky#0001"
};
const guild = buildGuild();
const channel = buildChannel({
guild,
channelId,
channelSendPayloads,
typingCallsRef
});
const message = buildIncomingMessage({
guild,
channel,
messageId: "msg-gif-question",
content: "clanky, what is this gif showing?",
replyPayloads
});
const settings = store.getSettings();
const runtime = buildReplyPipelineRuntime(bot, {
captionTimestamps: [],
unsolicitedReplyContextWindow: 2
});
const recentMessages = [
{
message_id: "msg-gif-link",
author_name: "alice",
content: "https://media.tenor.com/rAWXcy9O6XUAAAPo/wow-taiko.mp4",
created_at: new Date().toISOString(),
is_bot: false
},
{
message_id: "msg-gif-question",
author_name: "alice",
content: "clanky, what is this gif showing?",
created_at: new Date().toISOString(),
is_bot: false
}
];
const handled = await maybeReplyToMessagePipeline(runtime, message as never, settings, {
source: "message_event_coalesced",
forceDecisionLoop: true,
forceRespond: true,
recentMessages,
triggerMessageIds: ["msg-gif-link", String(message.id || "")],
addressSignal: {
direct: true,
inferred: true,
triggered: true,
reason: "name_exact"
}
});
assert.equal(handled, true);
assert.equal(llmCalls[0]?.imageInputs.length, 0);
assert.match(llmCalls[0]?.userPrompt || "", /Local runtime dependency missing \(ffmpeg\)/);
assert.match(llmCalls[0]?.userPrompt || "", /No GIF\/video pixels were inspected/);
assert.match(llmCalls[0]?.userPrompt || "", /operator setup issue/);
}); });
test("maybeReplyToMessagePipeline includes Minecraft docs, tool exposure, and active session hint", async () => { await withTempStore(async (store) => { const channelId = "chan-mc-1"; applyBaselineSettings(store, channelId); store.patchSettings({ agentStack: { runtimeConfig: { minecraft: { enabled: true } } } });
const llmCalls: Array<{
systemPrompt: string;
userPrompt: string;
tools: Array<{ name?: string }>;
}> = [];
const replyPayloads: Array<Record<string, unknown>> = [];
const channelSendPayloads: Array<Record<string, unknown>> = [];
const typingCallsRef = { count: 0 };
const bot = new ClankerBot({
appConfig: {},
store,
llm: {
async generate(payload) {
llmCalls.push({
systemPrompt: String(payload.systemPrompt || ""),
userPrompt: String(payload.userPrompt || ""),
tools: Array.isArray(payload.tools) ? payload.tools as Array<{ name?: string }> : []
});
return {
text: JSON.stringify({
text: "Minecraft noted.",
skip: false,
reactionEmoji: null,
media: null,
automationAction: {
operation: "none",
title: null,
instruction: null,
schedule: null,
targetQuery: null,
automationId: null,
runImmediately: false,
targetChannelId: null
},
screenWatchIntent: {
action: "none",
confidence: 0,
reason: null
}
}),
toolCalls: [],
rawContent: null,
provider: "claude-oauth",
model: "claude-opus-4-6",
usage: {
inputTokens: 10,
outputTokens: 10,
cacheWriteTokens: 0,
cacheReadTokens: 0
},
costUsd: 0
};
}
},
memory: null,
discovery: null,
search: null,
gifs: null,
video: null
});
bot.client.user = {
id: "bot-1",
username: "clanky",
tag: "clanky#0001"
};
const minecraftSession: SubAgentSession & { getPromptStateHint(): string } = {
id: "minecraft:guild-1:chan-mc-1:1:1",
type: "minecraft",
createdAt: Date.now(),
ownerUserId: "user-1",
lastUsedAt: Date.now(),
status: "idle",
getPromptStateHint() {
return "[Minecraft] Active session - goal: \"Stay with Steve\" | mode: companion | server: Survival SMP | connected: yes | last action: Following Steve.";
},
async runTurn() {
throw new Error("not used");
},
cancel() {},
close() {}
};
bot.subAgentSessions.register(minecraftSession);
const guild = buildGuild();
const channel = buildChannel({
guild,
channelId,
channelSendPayloads,
typingCallsRef
});
const message = buildIncomingMessage({
guild,
channel,
messageId: "msg-mc-1",
content: "what's going on in Minecraft?",
replyPayloads
});
const settings = store.getSettings();
const runtime = buildReplyPipelineRuntime(bot, {
captionTimestamps: [],
unsolicitedReplyContextWindow: 2
});
const handled = await maybeReplyToMessagePipeline(runtime, message, settings, {
source: "message_event",
forceDecisionLoop: true,
forceRespond: true,
recentMessages: [],
triggerMessageIds: [message.id],
addressSignal: {
direct: true,
inferred: false,
triggered: true,
reason: "direct_address"
}
});
assert.equal(handled, true);
assert.equal(typingCallsRef.count, 1);
assert.match(llmCalls[0]?.systemPrompt || "", /=== MINECRAFT ===/);
assert.match(llmCalls[0]?.systemPrompt || "", /hand over the user's intent or relevant context/i);
assert.match(llmCalls[0]?.userPrompt || "", /\[Minecraft\] Active session - goal: "Stay with Steve"/);
assert.equal(llmCalls[0]?.tools.some((tool) => tool?.name === "minecraft_task"), true);
}); });
