项目文件夹

文件
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

153 行
6.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { materializeRoster } from '@/lib/edit/agent-roster';
import type { GeneratedAgentConfig } from '@/lib/types/stage';
// ----- helpers ---------------------------------------------------------------
function makeConfig(id: string, role = 'teacher'): GeneratedAgentConfig {
return {
id,
name: id,
role,
persona: '',
avatar: '/avatars/teacher.png',
color: '#000',
priority: role === 'teacher' ? 10 : role === 'assistant' ? 7 : 5,
};
}
/** Deterministic counter-based id factory. */
function makeCounter(): () => string {
let n = 0;
return () => `gen-${++n}`;
}
/** Stub resolvePreset backed by an id→config map; returns undefined for unknowns. */
function makeResolver(
entries: GeneratedAgentConfig[],
): (id: string) => GeneratedAgentConfig | undefined {
const map = new Map(entries.map((e) => [e.id, e]));
return (id) => map.get(id);
}
// ----- test cases ------------------------------------------------------------
describe('materializeRoster – (a) generatedAgentConfigs present', () => {
it('returns existing generatedAgentConfigs array unchanged', () => {
const configs: GeneratedAgentConfig[] = [
makeConfig('t1', 'teacher'),
makeConfig('s1', 'student'),
];
const stage = { generatedAgentConfigs: configs };
const result = materializeRoster(stage, () => undefined, makeCounter());
expect(result).toBe(configs); // same reference, not a copy
expect(result).toHaveLength(2);
expect(result[0].id).toBe('t1');
});
it('returns existing configs even when agentIds is also present', () => {
const configs: GeneratedAgentConfig[] = [makeConfig('t1', 'teacher')];
const stage = { generatedAgentConfigs: configs, agentIds: ['preset-1'] };
const result = materializeRoster(
stage,
makeResolver([makeConfig('preset-1', 'student')]),
makeCounter(),
);
expect(result).toBe(configs);
});
it('returns same reference even for an unusual student-only generatedAgentConfigs (no teacher)', () => {
// Branch 1 deliberately skips the ≥1-teacher invariant for stored rosters —
// the editor's last-teacher guard maintains that separately.
const configs: GeneratedAgentConfig[] = [
makeConfig('s1', 'student'),
makeConfig('s2', 'student'),
];
const stage = { generatedAgentConfigs: configs };
const result = materializeRoster(stage, () => undefined, makeCounter());
expect(result).toBe(configs); // same reference, no mutation or teacher injection
});
});
describe('materializeRoster – (b) agentIds resolved via resolvePreset', () => {
it('assigns fresh ids for global-default preset ids (isGlobalDefault → true)', () => {
const presets = [makeConfig('a1', 'teacher'), makeConfig('a2', 'student')];
const stage = { agentIds: ['a1', 'a2'] };
const result = materializeRoster(stage, makeResolver(presets), makeCounter(), () => true);
// Global defaults get fresh stage-scoped ids, never the global preset ids
expect(result.map((r) => r.id)).toEqual(['gen-1', 'gen-2']);
expect(result.map((r) => r.id)).not.toContain('a1');
expect(result.map((r) => r.id)).not.toContain('a2');
// Other fields must be preserved from the resolved preset
expect(result[0].name).toBe('a1');
expect(result[0].role).toBe('teacher');
expect(result[1].name).toBe('a2');
expect(result[1].role).toBe('student');
});
it('keeps original id for stage-generated ids (isGlobalDefault → false)', () => {
const presets = [makeConfig('gen-abc', 'teacher'), makeConfig('gen-xyz', 'student')];
const stage = { agentIds: ['gen-abc', 'gen-xyz'] };
const result = materializeRoster(stage, makeResolver(presets), makeCounter(), () => false);
// Stage-generated ids are preserved so scene references remain valid
expect(result.map((r) => r.id)).toEqual(['gen-abc', 'gen-xyz']);
expect(result[0].name).toBe('gen-abc');
expect(result[0].role).toBe('teacher');
expect(result[1].name).toBe('gen-xyz');
expect(result[1].role).toBe('student');
});
it('drops ids that resolve to undefined', () => {
const presets = [makeConfig('a1', 'teacher')];
const stage = { agentIds: ['a1', 'unknown-x'] };
const result = materializeRoster(stage, makeResolver(presets), makeCounter(), () => true);
// Only the resolved entry survives, with a fresh id (a1 is a global default here)
expect(result.map((r) => r.id)).toEqual(['gen-1']);
expect(result[0].name).toBe('a1');
});
});
describe('materializeRoster – (c) empty stage → single default teacher', () => {
it('returns a single teacher config when stage has no configs', () => {
const result = materializeRoster({}, () => undefined, makeCounter());
expect(result).toHaveLength(1);
expect(result[0].role).toBe('teacher');
expect(result[0].id).toBe('gen-1');
});
it('returns a single teacher config when agentIds is empty array', () => {
const result = materializeRoster({ agentIds: [] }, () => undefined, makeCounter());
expect(result).toHaveLength(1);
expect(result[0].role).toBe('teacher');
});
it('returns a single teacher config when generatedAgentConfigs is empty array', () => {
const result = materializeRoster({ generatedAgentConfigs: [] }, () => undefined, makeCounter());
expect(result).toHaveLength(1);
expect(result[0].role).toBe('teacher');
});
});
describe('materializeRoster – (d) result always has ≥1 teacher', () => {
it('resolved presets that include a teacher satisfy the invariant', () => {
const presets = [makeConfig('t1', 'teacher'), makeConfig('s1', 'student')];
const stage = { agentIds: ['t1', 's1'] };
const result = materializeRoster(stage, makeResolver(presets), makeCounter(), () => true);
const teachers = result.filter((a) => a.role === 'teacher');
expect(teachers.length).toBeGreaterThanOrEqual(1);
});
it('prepends a teacher when resolved presets contain no teacher', () => {
const presets = [makeConfig('s1', 'student'), makeConfig('s2', 'student')];
const stage = { agentIds: ['s1', 's2'] };
const result = materializeRoster(stage, makeResolver(presets), makeCounter(), () => true);
const teachers = result.filter((a) => a.role === 'teacher');
expect(teachers.length).toBeGreaterThanOrEqual(1);
});
it('empty stage fallback roster always contains a teacher', () => {
const result = materializeRoster({}, () => undefined, makeCounter());
expect(result.some((a) => a.role === 'teacher')).toBe(true);
});
});