Files
zalbot/tests/unit/contextAssembler.test.ts
Kaysser Kayyali 9dc6e8e1a3 Initial commit — Mardonar encounter engine with UX improvements
Includes full bot source (Phases 1–4), plus five new features:
- Epic 1: emoji reaction state machine (👀🎲) + burst queue cap at 2 with in-world drop notices
- Epic 2: per-encounter tone field in YAML injected into LLM system prompt
- Epic 3: player pronouns via modal registration + system prompt players block
- Epic 4: strengthened skill_check_emit tool contract + missed-skill-check diagnostic

Also includes UX design docs, epics, and story files under Docs/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 04:51:21 +00:00

85 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import { assembleContext } from '../../src/harness/contextAssembler.js';
import { mockSession, mockSpec } from '../fixtures/spec.js';
import type { SessionState, ChatMessage } from '../../src/types/index.js';
function makeMessage(role: ChatMessage['role'], content: string, pinned = false): ChatMessage {
return { role, content, pinned, timestamp: Date.now() };
}
describe('assembleContext', () => {
it('puts the system message first', () => {
const context = assembleContext(mockSession);
expect(context[0].role).toBe('system');
});
it('includes the system prompt content', () => {
const context = assembleContext(mockSession);
expect(context[0].content).toContain('narrator');
});
it('always includes pinned messages after system', () => {
const session: SessionState = {
...mockSession,
history: [
makeMessage('assistant', 'Opening narrative.', true),
makeMessage('user', 'Player action.'),
makeMessage('assistant', 'LLM response.'),
],
};
const context = assembleContext(session);
const pinned = context.filter(m => m.pinned && m.role !== 'system');
expect(pinned).toHaveLength(1);
expect(pinned[0].content).toBe('Opening narrative.');
});
it('includes sliding history messages', () => {
const session: SessionState = {
...mockSession,
history: [
makeMessage('user', 'Player says something.'),
makeMessage('assistant', 'Narrator responds.'),
],
};
const context = assembleContext(session);
const nonSystem = context.filter(m => !m.pinned);
expect(nonSystem.some(m => m.content === 'Player says something.')).toBe(true);
});
it('injects NPC memory into the system prompt', () => {
const session: SessionState = {
...mockSession,
npcMemories: {
'npc-one': 'Past encounters witnessed:\n - [2026-01-01] Tavern Brawl: A fight broke out.',
},
};
const context = assembleContext(session);
expect(context[0].content).toContain('Tavern Brawl');
});
it('drops oldest non-pinned pairs when history exceeds budget', () => {
// Use natural language so BPE tokenisation produces realistic token counts.
// Repeated single characters compress to almost nothing in BPE.
const bigContent = 'the quick brown fox jumps over the lazy dog. '.repeat(100);
const history: ChatMessage[] = [
makeMessage('assistant', 'Opening narrative pinned.', true),
];
// 200 pairs × ~1 000 tokens each ≈ 200 000 tokens >> 114 500 budget
for (let i = 0; i < 200; i++) {
history.push(makeMessage('user', `${bigContent} turn ${i}`));
history.push(makeMessage('assistant', `${bigContent} response ${i}`));
}
const session: SessionState = { ...mockSession, history };
const context = assembleContext(session);
// Pinned message must survive trimming
const pinnedInContext = context.filter(m => m.pinned && m.role !== 'system');
expect(pinnedInContext).toHaveLength(1);
// Sliding window should be well under the 400 we pushed in
const sliding = context.filter(m => !m.pinned);
expect(sliding.length).toBeLessThan(400);
});
});