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

71 lines
3.7 KiB
JavaScript

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);
});
});
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');
});
});