Files
brain/tools/secretary-worker.test.mjs
T

82 lines
4.5 KiB
JavaScript
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, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { runWorker, pickModel } from './secretary-worker.mjs';
import { enqueueSpan, readCursor } from './secretary-queue.mjs';
import { EMPTY_PROTOCOL } from './secretary-protocol.mjs';
let dir, secdir, workDir;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'secw-'));
secdir = join(dir, 'docs', 'secretary'); workDir = join(secdir, 'тема');
mkdirSync(join(secdir, 'raw'), { recursive: true }); mkdirSync(workDir, { recursive: true });
writeFileSync(join(workDir, 'protocol.json'), JSON.stringify(EMPTY_PROTOCOL()));
writeFileSync(join(secdir, 'raw', 's.log'), '=== ХОД turn=1 ===\n[ЮЗЕР]\nпривет\n[АССИСТЕНТ]\nответ\n=== КОНЕЦ ХОДА ===\n');
});
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
const deps = (overrides = {}) => ({
secdir,
distill: async (proto) => ({ ...proto, subject: 'разобрано', steps: [{ turn: 1, text: 'шаг' }] }),
render: () => '# протокол',
now: () => 1000,
...overrides,
});
describe('runWorker', () => {
it('drains one queued span: writes protocol, renders md, advances cursor', async () => {
enqueueSpan(workDir, { session: 's', span: { start: 1, end: 1, index: 0, note: '' }, kind: 'span' });
await runWorker(workDir, deps());
const proto = JSON.parse(readFileSync(join(workDir, 'protocol.json'), 'utf-8'));
expect(proto.subject).toBe('разобрано');
expect(readFileSync(join(workDir, 'protocol.md'), 'utf-8')).toContain('протокол');
expect(readCursor(workDir)).toBe(0);
});
it('skips a job whose index <= cursor (idempotent re-enqueue)', async () => {
let distillCalls = 0;
enqueueSpan(workDir, { session: 's', span: { start: 1, end: 1, index: 0, note: '' }, kind: 'span' });
enqueueSpan(workDir, { session: 's', span: { start: 1, end: 1, index: 0, note: '' }, kind: 'span' });
await runWorker(workDir, deps({ distill: async (p) => { distillCalls++; return p; } }));
expect(distillCalls).toBe(1);
});
it('exits immediately if the theme lock is held', async () => {
let distillCalls = 0;
enqueueSpan(workDir, { session: 's', span: { start: 1, end: 1, index: 0, note: '' }, kind: 'span' });
await runWorker(workDir, deps({ acquire: async () => null, distill: async (p) => { distillCalls++; return p; } }));
expect(distillCalls).toBe(0);
});
it('degrades on distill throw: cursor still advances, protocol not corrupted', async () => {
enqueueSpan(workDir, { session: 's', span: { start: 1, end: 1, index: 0, note: '' }, kind: 'span' });
await runWorker(workDir, deps({ distill: async () => { throw new Error('context window exceeded'); } }));
expect(readCursor(workDir)).toBe(0);
expect(existsSync(join(workDir, 'protocol.json'))).toBe(true);
});
it('под fluffy-флагом пишет архив.md рядом с protocol.md', async () => {
enqueueSpan(workDir, { session: 's', span: { start: 1, end: 1, index: 0, note: '' }, kind: 'span' });
await runWorker(workDir, deps({ writeArchive: true, archive: () => '# архив-леджер' }));
expect(existsSync(join(workDir, 'архив.md'))).toBe(true);
expect(readFileSync(join(workDir, 'архив.md'), 'utf-8')).toContain('архив-леджер');
});
it('без fluffy-флага архив.md НЕ пишется', async () => {
enqueueSpan(workDir, { session: 's', span: { start: 1, end: 1, index: 0, note: '' }, kind: 'span' });
await runWorker(workDir, deps({ writeArchive: false }));
expect(existsSync(join(workDir, 'архив.md'))).toBe(false);
});
});
describe('pickModel (per-role model forwarding)', () => {
it('uses req.model (per-role) when present', () => {
expect(pickModel({ system: 's', user: 'u', model: 'role-model' }, { SECRETARY_LLM_MODEL: 'global' })).toBe('role-model');
});
it('falls back to SECRETARY_LLM_MODEL when req has no model', () => {
expect(pickModel({ system: 's', user: 'u' }, { SECRETARY_LLM_MODEL: 'global' })).toBe('global');
});
it('returns undefined when neither present (callAnthropicAPI default applies)', () => {
expect(pickModel({ system: 's', user: 'u' }, {})).toBe(undefined);
});
it('handles a string prompt (old audit path) → falls back to env', () => {
expect(pickModel('just a string', { SECRETARY_LLM_MODEL: 'global' })).toBe('global');
});
});