test: stabilize channel mode and discussion coverage

This commit is contained in:
zhi
2026-04-02 06:18:16 +00:00
parent 4e0a24333e
commit b11c15d8c8
6 changed files with 113 additions and 26 deletions

View File

@@ -5,7 +5,7 @@ import os from 'node:os';
import path from 'node:path';
import { createDiscussionService } from '../plugin/core/discussion-service.ts';
import { buildDiscussionClosedMessage, buildDiscussionOriginCallbackMessage } from '../plugin/core/discussion-messages.ts';
import { buildDiscussionClosedMessage, buildDiscussionIdleReminderMessage, buildDiscussionOriginCallbackMessage } from '../plugin/core/discussion-messages.ts';
function makeLogger() {
return {
@@ -246,6 +246,44 @@ test('handleCallback rejects an absolute path outside the initiator workspace',
);
});
test('maybeSendIdleReminder sends exactly one idle reminder for an active discussion', async () => {
const workspace = makeWorkspace();
const fetchCalls: Array<{ url: string; body: any }> = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
const body = init?.body ? JSON.parse(String(init.body)) : undefined;
fetchCalls.push({ url: String(url), body });
return new Response(JSON.stringify({ id: `msg-${fetchCalls.length}` }), { status: 200 });
}) as typeof fetch;
try {
const service = createDiscussionService({
api: makeApi() as any,
workspaceRoot: workspace,
moderatorBotToken: 'bot-token',
forceNoReplyForSession: () => {},
});
await service.initDiscussion({
discussionChannelId: 'discussion-idle-1',
originChannelId: 'origin-idle-1',
initiatorAgentId: 'agent-idle',
initiatorSessionId: 'session-idle',
discussGuide: 'Only send one reminder.',
});
await service.maybeSendIdleReminder('discussion-idle-1');
await service.maybeSendIdleReminder('discussion-idle-1');
assert.equal(fetchCalls.length, 2);
assert.equal(fetchCalls[1]?.url, 'https://discord.com/api/v10/channels/discussion-idle-1/messages');
assert.equal(fetchCalls[1]?.body?.content, buildDiscussionIdleReminderMessage());
} finally {
globalThis.fetch = originalFetch;
}
});
test('handleCallback notifies the origin channel with the resolved summary path', async () => {
const workspace = makeWorkspace();
const summaryRelPath = path.join('plans', 'discussion-summary.md');

View File

@@ -1,7 +1,7 @@
import { describe, it, beforeEach, afterEach } from "node:test";
import assert from "node:assert";
import { enterMultiMessageMode, exitMultiMessageMode, isMultiMessageMode, setChannelShuffling, getChannelShuffling } from "../plugin/core/channel-modes.ts";
import { initTurnOrder, checkTurn, onNewMessage, resetTurn, setWaitingForHuman, isWaitingForHuman, onSpeakerDone } from "../plugin/turn-manager.ts";
import { initTurnOrder, checkTurn, getTurnDebugInfo, onNewMessage, resetTurn, setWaitingForHuman, isWaitingForHuman } from "../plugin/turn-manager.ts";
describe("Mode Compatibility Tests", () => {
const channelId = "test-channel";
@@ -72,6 +72,7 @@ describe("Mode Compatibility Tests", () => {
// In real implementation, mention override would be set via setMentionOverride function
// This test ensures the settings coexist properly
const state = getTurnDebugInfo(channelId);
assert.ok(state.hasTurnState);
assert.strictEqual(getChannelShuffling(channelId), true);
});
});

View File

@@ -1,7 +1,7 @@
import { describe, it, beforeEach, afterEach } from "node:test";
import assert from "node:assert";
import { enterMultiMessageMode, exitMultiMessageMode, isMultiMessageMode } from "../plugin/core/channel-modes.ts";
import { initTurnOrder, checkTurn, onNewMessage, resetTurn } from "../plugin/turn-manager.ts";
import { initTurnOrder, checkTurn, getTurnDebugInfo, onNewMessage, resetTurn } from "../plugin/turn-manager.ts";
describe("Multi-Message Mode Tests", () => {
const channelId = "test-channel";
@@ -70,8 +70,9 @@ describe("Multi-Message Mode Tests", () => {
// Even with mention override conceptually, multi-message mode should take precedence
// In real usage, mention overrides happen in message-received hook before multi-message mode logic
const turnResult = checkTurn(channelId, "agent-a");
assert.ok(typeof turnResult === "object");
// The actual behavior depends on the before-model-resolve hook which forces no-reply in multi-message mode
// Exit multi-message mode to resume normal operation
exitMultiMessageMode(channelId);
assert.strictEqual(isMultiMessageMode(channelId), false);

View File

@@ -117,15 +117,16 @@ describe("Shuffle Mode Tests", () => {
// Enable shuffling
setChannelShuffling(channelId, true);
// Single agent should work fine
// Dormant channels need a new message to activate the first speaker.
onNewMessage(channelId, "human-user", true);
const turnResult = checkTurn(channelId, "agent-a");
assert.strictEqual(turnResult.allowed, true);
onSpeakerDone(channelId, "agent-a", false);
// Should still work with single agent after reshuffle attempt
const turnResultAfter = checkTurn(channelId, "agent-a");
assert.strictEqual(turnResultAfter.allowed, true);
const stateAfter = getTurnDebugInfo(channelId);
assert.deepStrictEqual(stateAfter.turnOrder, ["agent-a"]);
assert.strictEqual(stateAfter.currentSpeaker, "agent-a");
});
it("should handle double agent scenario properly", () => {
@@ -135,6 +136,9 @@ describe("Shuffle Mode Tests", () => {
// Enable shuffling
setChannelShuffling(channelId, true);
// Activate the channel before exercising the round transition.
onNewMessage(channelId, "human-user", true);
const initialOrder = getTurnDebugInfo(channelId).turnOrder as string[];
const firstSpeaker = initialOrder[0];
const secondSpeaker = initialOrder[1];
@@ -146,14 +150,14 @@ describe("Shuffle Mode Tests", () => {
onSpeakerDone(channelId, secondSpeaker, false);
// The order might be reshuffled, but it should be valid
const newOrder = getTurnDebugInfo(channelId).turnOrder as string[];
const newState = getTurnDebugInfo(channelId);
const newOrder = newState.turnOrder as string[];
assert.strictEqual(newOrder.length, 2);
assert.ok(newOrder.includes("agent-a"));
assert.ok(newOrder.includes("agent-b"));
// Next speaker should be determined by the new order
const nextSpeaker = advanceTurn(channelId);
assert.ok(["agent-a", "agent-b"].includes(nextSpeaker as string));
// After a full round, the next current speaker should already be set.
assert.ok(["agent-a", "agent-b"].includes(newState.currentSpeaker as string));
});
});