Files
connectai/tests/officeSchema.test.ts
T

242 lines
9.7 KiB
TypeScript

import {
validateOfficeSnapshot,
makeEmptyOfficeSnapshot,
} from '../src/features/astraOffice/schema';
import {
validateLayout,
migrateLayout,
} from '../src/features/astraOffice/view/layoutSchema';
import { presentOfficeSnapshot, normalizeAgentId } from '../src/features/astraOffice/presenter';
describe('OfficeSnapshot schema', () => {
test('makeEmptyOfficeSnapshot returns idle / null active', () => {
const s = makeEmptyOfficeSnapshot();
expect(s.phase).toBe('idle');
expect(s.activeAgentId).toBeNull();
expect(s.roster).toEqual([]);
expect(s.activity).toEqual([]);
expect(s.newBubbles).toEqual([]);
});
test('validateOfficeSnapshot rejects null / non-object', () => {
expect(validateOfficeSnapshot(null)).toBeNull();
expect(validateOfficeSnapshot(undefined)).toBeNull();
expect(validateOfficeSnapshot('idle')).toBeNull();
expect(validateOfficeSnapshot(42)).toBeNull();
});
test('validateOfficeSnapshot fills defaults for sparse input', () => {
const s = validateOfficeSnapshot({});
expect(s).not.toBeNull();
expect(s!.phase).toBe('idle');
expect(s!.activeAgentId).toBeNull();
expect(s!.roster).toEqual([]);
expect(typeof s!.updatedAt).toBe('number');
});
test('validateOfficeSnapshot rejects invalid enums quietly', () => {
const s = validateOfficeSnapshot({
phase: 'totally_made_up_phase',
activeAgentId: 'ceo',
roster: [{ agentId: 'ceo', agentName: 'CEO', roleCategory: 'BOGUS', status: 'NOT_REAL', lastActivityAt: 100 }],
});
expect(s!.phase).toBe('idle'); // fell back
expect(s!.roster[0].roleCategory).toBe('support'); // fell back
expect(s!.roster[0].status).toBe('idle'); // fell back
});
test('validateOfficeSnapshot keeps valid roster + bubbles', () => {
const s = validateOfficeSnapshot({
phase: 'executing',
activeAgentId: 'developer',
roster: [
{ agentId: 'developer', agentName: '개발', roleCategory: 'developer', status: 'executing', lastActivityAt: 100 },
{ agentId: 'inspector', agentName: '감리', roleCategory: 'inspector', status: 'idle', lastActivityAt: 90 },
],
newBubbles: [
{ agentId: 'developer', text: '코드 들어간다', type: 'event' },
{ agentId: 'inspector', text: 'should_be_dropped', type: 'wrong_type' }, // type 잘못 → 'status' 로 폴백
],
});
expect(s!.phase).toBe('executing');
expect(s!.activeAgentId).toBe('developer');
expect(s!.roster).toHaveLength(2);
expect(s!.newBubbles).toHaveLength(2);
expect(s!.newBubbles[1].type).toBe('status'); // fell back
});
test('validateOfficeSnapshot drops malformed roster entries', () => {
const s = validateOfficeSnapshot({
phase: 'idle',
roster: [
{ agentId: 'ok', roleCategory: 'ceo', status: 'idle', lastActivityAt: 0 },
{ /* no agentId */ roleCategory: 'ceo' },
null,
'string',
],
});
expect(s!.roster).toHaveLength(1);
expect(s!.roster[0].agentId).toBe('ok');
});
});
describe('Layout schema', () => {
test('validateLayout rejects non-v2 raw', () => {
expect(validateLayout(null)).toBeNull();
expect(validateLayout({ cells: [] })).toBeNull(); // empty cells, no schema marker → null
expect(validateLayout({ cells: [{ roleKey: 'x', deskX: 0, deskY: 0 }] })).toBeNull(); // missing v2 markers
});
test('validateLayout accepts explicit schema:2', () => {
const v = validateLayout({
schema: 2,
cells: [
{
roleKey: 'ceo',
agentKey: 'ceo',
label: 'CEO',
charRow: 0,
deskSprite: 'desk-boss',
face: 'R',
boss: true,
deskX: 100, deskY: 50, deskW: 136,
seatX: 130, seatY: 80,
deskRot: 0, deskZ: 0, charRot: 0, charZ: 0,
noChar: false,
},
],
objs: [{ id: 'obj_0', name: 'plant', x: 10, y: 20, w: 32, rot: 0, z: 0 }],
});
expect(v).not.toBeNull();
expect(v!.cells).toHaveLength(1);
expect(v!.cells[0].deskSprite).toBe('desk-boss');
expect(v!.objs[0].name).toBe('plant');
});
test('validateLayout normalizes invalid face to R', () => {
const v = validateLayout({
schema: 2,
cells: [{
roleKey: 'x', deskSprite: 'desk-main', face: 'XYZ', charRow: 0,
deskX: 0, deskY: 0, deskW: 100, seatX: 0, seatY: 0,
deskRot: 0, deskZ: 0, charRot: 0, charZ: 0,
}],
});
expect(v!.cells[0].face).toBe('R');
});
test('validateLayout detects v2 via field presence (no explicit schema)', () => {
const v = validateLayout({
cells: [{
roleKey: 'a', deskSprite: 'desk-main', charRow: 2,
deskX: 0, deskY: 0, deskW: 100, seatX: 0, seatY: 0,
}],
});
expect(v).not.toBeNull();
expect(v!.cells[0].charRow).toBe(2);
});
test('migrateLayout upgrades v1 (coord-only) cells', () => {
const v1 = {
cells: [{ roleKey: 'ceo', deskX: 100, deskY: 50, deskW: 136, seatX: 130, seatY: 80, deskRot: 0, deskZ: 0, charRot: 0, charZ: 0 }],
objs: [],
};
const v = migrateLayout(v1);
expect(v).not.toBeNull();
expect(v!.schema).toBe(2);
expect(v!.cells[0].agentKey).toBe('ceo'); // fallback: roleKey == agentKey
expect(v!.cells[0].deskSprite).toBe('desk-main');
expect(v!.cells[0].charRow).toBe(0);
});
});
describe('presenter', () => {
test('normalizeAgentId resolves aliases', () => {
expect(normalizeAgentId('writer')).toBe('writer');
expect(normalizeAgentId('Editor')).toBe('designer');
expect(normalizeAgentId('Secretary')).toBe('support');
expect(normalizeAgentId('business')).toBe('inspector');
expect(normalizeAgentId(undefined)).toBeNull();
});
test('presentOfficeSnapshot builds single-agent roster from old AgentWorkState', () => {
const snap = presentOfficeSnapshot({
activeState: {
agentId: 'inspector',
agentName: '감리',
status: 'reviewing',
currentStep: '라운드 2/3',
currentTask: '타입 정합성 검수',
recentLogs: ['타입 누락 발견'],
updatedAt: 1000,
} as any,
});
expect(snap.phase).toBe('reviewing');
expect(snap.activeAgentId).toBe('inspector');
expect(snap.roster).toHaveLength(1);
expect(snap.roster[0].roleCategory).toBe('inspector');
expect(snap.roster[0].lastLog).toBe('타입 누락 발견');
expect(snap.task?.goal).toBe('타입 정합성 검수');
});
test('presentOfficeSnapshot returns empty when no input', () => {
const snap = presentOfficeSnapshot({});
expect(snap.phase).toBe('idle');
expect(snap.activeAgentId).toBeNull();
expect(snap.roster).toEqual([]);
});
test('presentOfficeSnapshot maps need_clarification to awaiting-approval phase', () => {
const snap = presentOfficeSnapshot({
activeState: {
agentId: 'ceo',
agentName: 'CEO',
status: 'need_clarification',
needUserInput: ['배포 대상 환경은?'],
updatedAt: 0,
} as any,
});
expect(snap.phase).toBe('awaiting-approval');
expect(snap.awaiting?.kind).toBe('clarification');
expect(snap.awaiting?.questions).toEqual(['배포 대상 환경은?']);
});
test('presentOfficeSnapshot with roster places active agent in working state and others idle', () => {
const snap = presentOfficeSnapshot({
activeState: {
agentId: 'developer',
agentName: '개발',
status: 'executing',
currentStep: '함수 추출',
recentLogs: ['파일 수정 완료'],
updatedAt: 1000,
} as any,
roster: [
{ agentId: 'ceo', agentName: 'CEO', roleCategory: 'ceo' },
{ agentId: 'developer', agentName: '개발', roleCategory: 'developer' },
{ agentId: 'inspector', agentName: '감리', roleCategory: 'inspector' },
],
});
expect(snap.roster).toHaveLength(3);
const active = snap.roster.find((a) => a.agentId === 'developer');
const idle = snap.roster.find((a) => a.agentId === 'ceo');
expect(active?.status).toBe('executing');
expect(active?.lastLog).toBe('파일 수정 완료');
expect(idle?.status).toBe('idle');
expect(idle?.lastLog).toBeUndefined();
});
test('presentOfficeSnapshot folds recentActivity into snapshot.activity (ring buffer)', () => {
const activity = Array.from({ length: 40 }, (_, i) => ({
ts: i * 100,
agentId: 'developer',
text: `step ${i}`,
}));
const snap = presentOfficeSnapshot({ recentActivity: activity });
// presenter 가 32개로 잘라야 함.
expect(snap.activity).toHaveLength(32);
expect(snap.activity[0].text).toBe('step 8'); // 처음 8개 dropped
expect(snap.activity[31].text).toBe('step 39');
});
});