242 lines
9.7 KiB
TypeScript
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');
|
|
});
|
|
});
|