moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,238 @@
import { describe, it, expect } from 'vitest';
import { PERSONAS, getPersona, listPersonas, composePersonaPrompt } from '../src/personas.js';
describe('Agent Personas', () => {
it('PERSONAS catalog has 23 entries', () => {
// 22 user-facing (8 universal + 14 specialists) + 1 internal review persona
// (session-reviewer) used only by the IdleSessionWatcher self-evolution loop.
expect(PERSONAS).toHaveLength(23);
});
it('each persona has required fields', () => {
for (const persona of PERSONAS) {
expect(persona.id).toBeTruthy();
expect(persona.name).toBeTruthy();
expect(persona.description).toBeTruthy();
expect(persona.icon).toBeTruthy();
expect(persona.systemPrompt).toBeTruthy();
expect(persona.modelPreference).toBeTruthy();
expect(Array.isArray(persona.tools)).toBe(true);
expect(persona.tools.length).toBeGreaterThan(0);
expect(Array.isArray(persona.workspaceAffinity)).toBe(true);
expect(Array.isArray(persona.suggestedCommands)).toBe(true);
}
});
it('all persona IDs are unique', () => {
const ids = PERSONAS.map(p => p.id);
expect(new Set(ids).size).toBe(ids.length);
});
it('getPersona(id) returns the matching persona', () => {
const researcher = getPersona('researcher');
expect(researcher).not.toBeNull();
expect(researcher!.name).toBe('Researcher');
expect(researcher!.tools).toContain('web_search');
});
it('getPersona(nonexistent) returns null', () => {
expect(getPersona('nonexistent')).toBeNull();
expect(getPersona('')).toBeNull();
});
it('listPersonas() returns all 23 personas', () => {
const list = listPersonas();
expect(list).toHaveLength(23);
expect(list).not.toBe(PERSONAS); // Returns a copy
});
it('each persona systemPrompt is under 4000 chars', () => {
for (const persona of PERSONAS) {
expect(persona.systemPrompt.length).toBeLessThan(4000);
}
});
it('covers the 8 original roles', () => {
const ids = PERSONAS.map(p => p.id);
expect(ids).toContain('researcher');
expect(ids).toContain('writer');
expect(ids).toContain('analyst');
expect(ids).toContain('coder');
expect(ids).toContain('project-manager');
expect(ids).toContain('executive-assistant');
expect(ids).toContain('sales-rep');
expect(ids).toContain('marketer');
});
it('covers the 4 new universal/orchestration personas', () => {
const ids = PERSONAS.map(p => p.id);
expect(ids).toContain('general-purpose');
expect(ids).toContain('planner');
expect(ids).toContain('verifier');
expect(ids).toContain('coordinator');
});
it('every persona has failurePatterns with exactly 3 entries', () => {
for (const persona of PERSONAS) {
expect(persona.failurePatterns).toBeDefined();
expect(persona.failurePatterns).toHaveLength(3);
}
});
it('every persona has disallowedTools defined (array or undefined for soft-model personas)', () => {
const softModelIds = ['researcher', 'analyst'];
for (const persona of PERSONAS) {
if (softModelIds.includes(persona.id)) {
// Soft model personas should NOT have disallowedTools
expect(persona.disallowedTools, `${persona.id} should not have disallowedTools`).toBeUndefined();
}
}
});
it('read-only personas have isReadOnly true', () => {
const readOnlyIds = ['planner', 'verifier'];
for (const id of readOnlyIds) {
const persona = getPersona(id);
expect(persona).not.toBeNull();
expect(persona!.isReadOnly).toBe(true);
}
});
it('planner disallowedTools includes write_file', () => {
const planner = getPersona('planner');
expect(planner).not.toBeNull();
expect(planner!.disallowedTools).toContain('write_file');
});
it('verifier disallowedTools includes write_file', () => {
const verifier = getPersona('verifier');
expect(verifier).not.toBeNull();
expect(verifier!.disallowedTools).toContain('write_file');
});
it('coordinator tools has exactly 11 items', () => {
const coordinator = getPersona('coordinator');
expect(coordinator).not.toBeNull();
expect(coordinator!.tools).toHaveLength(11);
expect(coordinator!.tools).toContain('spawn_agent');
expect(coordinator!.tools).toContain('show_plan');
});
});
describe('Prompt composition', () => {
const corePrompt = 'You are Waggle, a workspace-native AI agent with persistent memory.';
it('composePersonaPrompt appends persona after separator', () => {
const persona = getPersona('researcher')!;
const result = composePersonaPrompt(corePrompt, persona);
expect(result).toContain(corePrompt);
expect(result).toContain('---');
expect(result).toContain('Persona: Researcher');
expect(result.indexOf(corePrompt)).toBeLessThan(result.indexOf('Persona: Researcher'));
});
it('combined prompt stays under 32000 chars', () => {
for (const persona of PERSONAS) {
const result = composePersonaPrompt(corePrompt, persona);
expect(result.length).toBeLessThanOrEqual(32000);
}
});
it('persona prompt is truncated if combined exceeds limit', () => {
const longCore = 'A'.repeat(30000);
const persona = getPersona('researcher')!;
// Set a tight limit so persona MUST be truncated (account for DOCX hint ~150 chars)
const result = composePersonaPrompt(longCore, persona, 30300);
expect(result.length).toBeLessThanOrEqual(30300);
expect(result).toContain('[...truncated]');
});
it('null persona returns core prompt with DOCX hint appended', () => {
const result = composePersonaPrompt(corePrompt, null);
expect(result).toContain(corePrompt);
expect(result).toContain('generate_docx');
});
});
describe('Persona field completeness', () => {
for (const persona of PERSONAS) {
it(`${persona.id} has tagline, bestFor, wontDo`, () => {
expect(persona.tagline, `${persona.id} missing tagline`).toBeTruthy();
expect(persona.bestFor, `${persona.id} missing bestFor`).toBeDefined();
expect(persona.bestFor!.length, `${persona.id} bestFor should have 3 items`).toBe(3);
expect(persona.wontDo, `${persona.id} missing wontDo`).toBeTruthy();
});
}
it('all personas have failurePatterns with at least 3 entries', () => {
for (const p of PERSONAS) {
expect(p.failurePatterns, `${p.id} missing failurePatterns`).toBeDefined();
expect(p.failurePatterns!.length, `${p.id} needs 3+ failure patterns`).toBeGreaterThanOrEqual(3);
}
});
});
describe('Soft tool model', () => {
it('researcher is NOT hard read-only', () => {
const r = getPersona('researcher');
expect(r!.isReadOnly).toBeFalsy();
expect(r!.disallowedTools).toBeUndefined();
});
it('analyst is NOT hard read-only', () => {
const a = getPersona('analyst');
expect(a!.isReadOnly).toBeFalsy();
expect(a!.disallowedTools).toBeUndefined();
});
it('planner still has hard disallowedTools', () => {
const p = getPersona('planner');
expect(p!.disallowedTools).toBeDefined();
expect(p!.disallowedTools).toContain('write_file');
expect(p!.isReadOnly).toBe(true);
});
it('verifier still has hard disallowedTools', () => {
const v = getPersona('verifier');
expect(v!.disallowedTools).toBeDefined();
expect(v!.disallowedTools).toContain('write_file');
expect(v!.isReadOnly).toBe(true);
});
it('session-reviewer is a trust-boundary persona: reads + create_skill only, no writes/exec/memory', () => {
const r = getPersona('session-reviewer');
expect(r, 'session-reviewer not found').toBeTruthy();
// create_skill is the proposal vehicle and must stay in the allowlist.
expect(r!.tools).toContain('create_skill');
// isReadOnly would strip create_skill, so it must be false.
expect(r!.isReadOnly).toBe(false);
// The critical tools ALWAYS_AVAILABLE_TOOLS re-adds must be explicitly denied.
for (const t of ['save_memory', 'delete_skill', 'install_capability', 'acquire_capability', 'spawn_agent', 'bash']) {
expect(r!.disallowedTools, `${t} must be disallowed`).toContain(t);
}
});
});
describe('New domain personas', () => {
const NEW_IDS = ['support-agent', 'ops-manager', 'data-engineer', 'recruiter', 'creative-director'];
for (const id of NEW_IDS) {
it(`${id} exists and has complete fields`, () => {
const p = getPersona(id);
expect(p, `${id} not found`).toBeTruthy();
expect(p!.tagline).toBeTruthy();
expect(p!.bestFor?.length).toBe(3);
expect(p!.wontDo).toBeTruthy();
expect(p!.failurePatterns?.length).toBeGreaterThanOrEqual(3);
expect(p!.systemPrompt.length).toBeGreaterThan(100);
expect(p!.tools.length).toBeGreaterThan(3);
});
}
it('total persona count is 23', () => {
// 22 tiered + session-reviewer (internal self-evolution reviewer).
expect(PERSONAS.length).toBe(23);
});
});