Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions app/classroom/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ export default function ClassroomDetailPage() {
});
log.info('Loaded from server-side storage:', classroomId);

// Hydrate server-generated agents into IndexedDB + registry
// Hydrate server-generated agents into IndexedDB + registry.
// Don't set selectedAgentIds here — the general agent
// restoration logic below (Path 2) handles it uniformly.
if (stage.generatedAgentConfigs?.length) {
const { saveGeneratedAgents } = await import('@/lib/orchestration/registry/store');
const { useSettingsStore } = await import('@/lib/store/settings');
const agentIds = await saveGeneratedAgents(stage.id, stage.generatedAgentConfigs);
useSettingsStore.getState().setSelectedAgentIds(agentIds);
log.info('Hydrated server-generated agents:', agentIds);
await saveGeneratedAgents(stage.id, stage.generatedAgentConfigs);
log.info('Hydrated server-generated agents for stage:', stage.id);
}
}
}
Expand Down
32 changes: 20 additions & 12 deletions lib/server/classroom-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export async function generateClassroom(

// Resolve agents based on agentMode
let agents: AgentInfo[];
const agentMode = input.agentMode || 'default';
let agentMode = input.agentMode || 'default';
if (agentMode === 'generate') {
log.info('Generating custom agent profiles via LLM...');
try {
Expand All @@ -238,6 +238,7 @@ export async function generateClassroom(
} catch (e) {
log.warn('Agent profile generation failed, falling back to defaults:', e);
agents = getDefaultAgents();
agentMode = 'default';
}
} else {
agents = getDefaultAgents();
Expand Down Expand Up @@ -328,17 +329,24 @@ export async function generateClassroom(
style: 'interactive',
createdAt: Date.now(),
updatedAt: Date.now(),
// Embed agent configs so API-generated classrooms can hydrate
// the client-side agent registry without IndexedDB
generatedAgentConfigs: agents.map((a, i) => ({
id: a.id,
name: a.name,
role: a.role,
persona: a.persona || '',
avatar: AGENT_DEFAULT_AVATARS[i % AGENT_DEFAULT_AVATARS.length],
color: AGENT_COLOR_PALETTE[i % AGENT_COLOR_PALETTE.length],
priority: a.role === 'teacher' ? 10 : a.role === 'assistant' ? 7 : 5,
})),
// For LLM-generated agents, embed full configs so the client can
// hydrate the agent registry without prior IndexedDB data.
// For default agents, just record IDs — the client already has them.
...(agentMode === 'generate'
? {
generatedAgentConfigs: agents.map((a, i) => ({
id: a.id,
name: a.name,
role: a.role,
persona: a.persona || '',
avatar: AGENT_DEFAULT_AVATARS[i % AGENT_DEFAULT_AVATARS.length],
color: AGENT_COLOR_PALETTE[i % AGENT_COLOR_PALETTE.length],
priority: a.role === 'teacher' ? 10 : a.role === 'assistant' ? 7 : 5,
})),
}
: {
agentIds: agents.map((a) => a.id),
}),
};

const store = createInMemoryStore(stage);
Expand Down
115 changes: 115 additions & 0 deletions tests/server/classroom-agent-mode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { describe, test, expect } from 'vitest';
/**
* Unit test for #353 fix: verify Stage object has correct agent fields
* based on agentMode.
*
* This doesn't call any LLM — it directly tests the conditional logic
* that was changed in classroom-generation.ts.
*/

import { getDefaultAgents } from '@/lib/orchestration/registry/store';
import { AGENT_COLOR_PALETTE, AGENT_DEFAULT_AVATARS } from '@/lib/constants/agent-defaults';

interface DefaultModeFields {
agentIds: string[];
}

interface GenerateModeFields {
generatedAgentConfigs: Array<{
id: string;
name: string;
role: string;
persona: string;
avatar: string;
color: string;
priority: number;
}>;
}

describe('#353: generatedAgentConfigs conditional on agentMode', () => {
// Replicate the Stage construction logic from classroom-generation.ts L322-349
function buildStageAgentFields(
agentMode: 'default' | 'generate',
agents: Array<{ id: string; name: string; role: string; persona?: string }>,
): DefaultModeFields | GenerateModeFields {
return agentMode === 'generate'
? {
generatedAgentConfigs: agents.map((a, i) => ({
id: a.id,
name: a.name,
role: a.role,
persona: a.persona || '',
avatar: AGENT_DEFAULT_AVATARS[i % AGENT_DEFAULT_AVATARS.length],
color: AGENT_COLOR_PALETTE[i % AGENT_COLOR_PALETTE.length],
priority: a.role === 'teacher' ? 10 : a.role === 'assistant' ? 7 : 5,
})),
}
: {
agentIds: agents.map((a) => a.id),
};
}

test('default mode should set agentIds, NOT generatedAgentConfigs', () => {
const agents = getDefaultAgents();
const fields = buildStageAgentFields('default', agents);

// Should have agentIds
expect(fields).toHaveProperty('agentIds');
expect((fields as DefaultModeFields).agentIds).toEqual([
'default-1',
'default-2',
'default-3',
'default-4',
'default-5',
'default-6',
]);

// Should NOT have generatedAgentConfigs
expect(fields).not.toHaveProperty('generatedAgentConfigs');
});

test('generate mode should set generatedAgentConfigs, NOT agentIds', () => {
const agents = [
{ id: 'gen-server-0', name: 'Prof. Li', role: 'teacher', persona: 'An expert' },
{ id: 'gen-server-1', name: 'Assistant', role: 'assistant', persona: 'Helpful' },
{ id: 'gen-server-2', name: 'Student', role: 'student', persona: 'Curious' },
];
const fields = buildStageAgentFields('generate', agents);

// Should have generatedAgentConfigs
expect(fields).toHaveProperty('generatedAgentConfigs');
expect((fields as GenerateModeFields).generatedAgentConfigs).toHaveLength(3);
expect((fields as GenerateModeFields).generatedAgentConfigs[0].id).toBe('gen-server-0');

// Should NOT have agentIds
expect(fields).not.toHaveProperty('agentIds');
});

test('generate mode with LLM fallback should behave like default mode', () => {
// Simulates: agentMode was 'generate', LLM failed, fell back to defaults
// After our fix, agentMode is reset to 'default' in the catch block
let agentMode: 'default' | 'generate' = 'generate';
let agents;

try {
throw new Error('Simulated LLM failure');
} catch {
agents = getDefaultAgents();
agentMode = 'default'; // ← This is our fix
}

const fields = buildStageAgentFields(agentMode, agents);

// Should behave exactly like default mode
expect(fields).toHaveProperty('agentIds');
expect(fields).not.toHaveProperty('generatedAgentConfigs');
expect((fields as DefaultModeFields).agentIds).toEqual([
'default-1',
'default-2',
'default-3',
'default-4',
'default-5',
'default-6',
]);
});
});
Loading