Files
openmaic/OpenMAIC/tests/lib/chat/pi/cue-user.test.ts
2026-08-16 14:58:47 +08:00

351 lines
10 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { buildCallAgentTool } from '@/lib/chat/pi/tools/call-agent';
import { buildCueUserTool } from '@/lib/chat/pi/tools/cue-user';
describe('Pi chat cue_user tool', () => {
it('emits the existing cue_user payload once', async () => {
const events: unknown[] = [];
let userCued = false;
const tool = buildCueUserTool({
getLastAgentId: () => 'default-1',
cueUser: async (data) => {
if (userCued) return false;
userCued = true;
events.push({ type: 'cue_user', data });
return true;
},
});
const first = await tool.execute('cue-1', { prompt: 'Any follow-up?' });
const second = await tool.execute('cue-2', { prompt: 'Again?' });
expect(events).toEqual([
{
type: 'cue_user',
data: { fromAgentId: 'default-1', prompt: 'Any follow-up?' },
},
]);
expect(first.details).toEqual({ emitted: true });
expect(second.details).toEqual({ emitted: false });
});
it('does not emit cue_user before any classroom agent turn when guarded', async () => {
const events: unknown[] = [];
const tool = buildCueUserTool({
getLastAgentId: () => undefined,
canCueUser: () => false,
cueUser: async (data) => {
events.push({ type: 'cue_user', data });
return true;
},
});
const result = await tool.execute('cue-1', { prompt: 'Any follow-up?' });
expect(events).toEqual([]);
expect(result.details).toEqual({
emitted: false,
skipped: true,
reason: 'no_agent_turns',
});
});
it('soft-skips cue_user with a teacher substantive reason when configured', async () => {
const events: unknown[] = [];
const tool = buildCueUserTool({
getLastAgentId: () => 'student-1',
canCueUser: () => false,
cueUserSkipReason: 'no_substantive_teacher_turn',
cueUser: async (data) => {
events.push({ type: 'cue_user', data });
return true;
},
});
const result = await tool.execute('cue-1', { prompt: 'Any follow-up?' });
expect(events).toEqual([]);
expect(result.details).toEqual({
emitted: false,
skipped: true,
reason: 'no_substantive_teacher_turn',
});
});
it('soft-skips cue_user with a teacher-or-assistant substantive reason when configured', async () => {
const events: unknown[] = [];
const tool = buildCueUserTool({
getLastAgentId: () => 'student-1',
canCueUser: () => false,
cueUserSkipReason: 'no_substantive_teaching_turn',
cueUser: async (data) => {
events.push({ type: 'cue_user', data });
return true;
},
});
const result = await tool.execute('cue-1', { prompt: 'Any follow-up?' });
expect(events).toEqual([]);
expect(result.content).toEqual([
{
type: 'text',
text: 'Call the teacher or teaching assistant for a visible answer before cueing the user.',
},
]);
expect(result.details).toEqual({
emitted: false,
skipped: true,
reason: 'no_substantive_teaching_turn',
});
});
it('allows cue_user after a teaching assistant substantive turn when guarded', async () => {
const events: unknown[] = [];
const tool = buildCueUserTool({
getLastAgentId: () => 'assistant-1',
canCueUser: () => true,
cueUserSkipReason: 'no_substantive_teaching_turn',
cueUser: async (data) => {
events.push({ type: 'cue_user', data });
return true;
},
});
const result = await tool.execute('cue-1', { prompt: 'Any follow-up?' });
expect(events).toEqual([
{
type: 'cue_user',
data: { fromAgentId: 'assistant-1', prompt: 'Any follow-up?' },
},
]);
expect(result.details).toEqual({ emitted: true });
});
it('allows cue_user after a teacher substantive turn when guarded', async () => {
const events: unknown[] = [];
const tool = buildCueUserTool({
getLastAgentId: () => 'teacher-1',
canCueUser: () => true,
cueUserSkipReason: 'no_substantive_teacher_turn',
cueUser: async (data) => {
events.push({ type: 'cue_user', data });
return true;
},
});
const result = await tool.execute('cue-1', { prompt: 'Any follow-up?' });
expect(events).toEqual([
{
type: 'cue_user',
data: { fromAgentId: 'teacher-1', prompt: 'Any follow-up?' },
},
]);
expect(result.details).toEqual({ emitted: true });
});
it('refuses cue_user after the session is closed', async () => {
const events: unknown[] = [];
const tool = buildCueUserTool({
getLastAgentId: () => 'default-1',
isSessionClosed: () => true,
cueUser: async (data) => {
events.push({ type: 'cue_user', data });
return true;
},
});
const result = await tool.execute('cue-1', { prompt: 'Any follow-up?' });
expect(events).toEqual([]);
expect(result.details).toEqual({
emitted: false,
skipped: true,
reason: 'session_closed',
});
});
it('skips call_agent after the user has already been cued', async () => {
const tool = buildCallAgentTool({
body: {
messages: [],
storeState: {
stage: { id: 'stage-1', name: 'Stage' },
scenes: [],
currentSceneId: null,
whiteboardOpen: false,
},
config: { agentIds: ['default-1'] },
apiKey: '',
} as never,
agentConfigs: [],
send: async () => {},
languageModel: {} as never,
onAgentDone: () => {},
onActionDone: () => {},
thinkingConfig: { mode: 'disabled', enabled: false },
abortSignal: new AbortController().signal,
maxAgentTurns: 6,
getAgentTurnCount: () => 0,
getAgentResponses: () => [],
getWhiteboardLedger: () => [],
maxActionsPerAgent: 1,
enableWhiteboardTools: false,
isUserCued: () => true,
});
const result = await tool.execute('call-1', {
agentId: 'default-1',
instruction: 'Please answer.',
});
expect(result.details).toEqual({ skipped: true, reason: 'user_already_cued' });
});
it('skips call_agent after the session is closed', async () => {
const tool = buildCallAgentTool({
body: {
messages: [],
storeState: {
stage: { id: 'stage-1', name: 'Stage' },
scenes: [],
currentSceneId: null,
whiteboardOpen: false,
},
config: { agentIds: ['default-1'] },
apiKey: '',
} as never,
agentConfigs: [],
send: async () => {},
languageModel: {} as never,
onAgentDone: () => {},
onActionDone: () => {},
thinkingConfig: { mode: 'disabled', enabled: false },
abortSignal: new AbortController().signal,
maxAgentTurns: 6,
getAgentTurnCount: () => 0,
getAgentResponses: () => [],
getWhiteboardLedger: () => [],
maxActionsPerAgent: 1,
enableWhiteboardTools: false,
isSessionClosed: () => true,
});
const result = await tool.execute('call-1', {
agentId: 'default-1',
instruction: 'Please answer.',
});
expect(result.details).toEqual({ skipped: true, reason: 'session_closed' });
});
it('counts rejected call_agent requests toward the hard attempt cap', async () => {
const tool = buildCallAgentTool({
body: {
messages: [],
storeState: {
stage: { id: 'stage-1', name: 'Stage' },
scenes: [],
currentSceneId: null,
whiteboardOpen: false,
},
config: { agentIds: ['default-1'] },
apiKey: '',
} as never,
agentConfigs: [],
send: async () => {},
languageModel: {} as never,
onAgentDone: () => {},
onActionDone: () => {},
thinkingConfig: { mode: 'disabled', enabled: false },
abortSignal: new AbortController().signal,
maxAgentTurns: 1,
getAgentTurnCount: () => 0,
getAgentResponses: () => [],
getWhiteboardLedger: () => [],
maxActionsPerAgent: 1,
enableWhiteboardTools: false,
});
for (let i = 0; i < 4; i += 1) {
await expect(
tool.execute(`invalid-${i}`, {
agentId: 'missing-agent',
instruction: 'Please answer.',
}),
).resolves.toMatchObject({
details: { skipped: true, reason: 'invalid_agent_id' },
});
}
await expect(
tool.execute('over-cap', {
agentId: 'missing-agent',
instruction: 'Please answer.',
}),
).resolves.toMatchObject({
details: { skipped: true, reason: 'agent_attempt_cap', totalAgentAttempts: 4 },
});
});
it('does not allow any call_agent after the agent turn budget is reached', async () => {
const tool = buildCallAgentTool({
body: {
messages: [],
storeState: {
stage: { id: 'stage-1', name: 'Stage' },
scenes: [],
currentSceneId: null,
whiteboardOpen: false,
},
config: { agentIds: ['teacher-1'] },
apiKey: '',
} as never,
agentConfigs: [
{
id: 'teacher-1',
name: 'Teacher',
role: 'teacher',
persona: 'Summarize clearly.',
avatar: '',
color: '#3366ff',
allowedActions: [],
priority: 10,
createdAt: new Date('2026-01-01T00:00:00Z'),
updatedAt: new Date('2026-01-01T00:00:00Z'),
isDefault: true,
},
],
send: async () => {},
languageModel: {} as never,
onAgentDone: () => {},
onActionDone: () => {},
thinkingConfig: { mode: 'disabled', enabled: false },
abortSignal: new AbortController().signal,
maxAgentTurns: 1,
getAgentTurnCount: () => 1,
getAgentResponses: () => [],
getWhiteboardLedger: () => [],
maxActionsPerAgent: 1,
enableWhiteboardTools: false,
});
expect(
(tool.parameters as { properties?: Record<string, unknown> }).properties,
).not.toHaveProperty('turnKind');
const result = await tool.execute('call-1', {
agentId: 'teacher-1',
instruction: 'Give a final summary.',
});
expect(result.details).toEqual({
skipped: true,
reason: 'agent_turn_limit',
maxAgentTurns: 1,
});
});
});