Files
portal/tools/observer-choice-detector.test.mjs
T
Дмитрий 7f379bd6a2 feat(observer): choice detector — user_chose_from_options kind
Pure module — extracts options (numbered/lettered/bullets/AskUserQuestion)
from last assistant message, detects user reference (position-based +
substring), returns decision_provenance for the 3rd kind.

23/23 tests GREEN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:57:36 +03:00

167 lines
6.9 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 } from 'vitest';
import { extractOptions, detectReference, detectChoiceProvenance } from './observer-choice-detector.mjs';
describe('extractOptions', () => {
it('returns null when content has fewer than 2 options', () => {
expect(extractOptions('Just one paragraph of text.')).toBeNull();
expect(extractOptions('1. only one item')).toBeNull();
});
it('extracts numbered list (1. ... 2. ...)', () => {
const text = '1. First option\n2. Second option\n3. Third option';
expect(extractOptions(text)).toEqual(['First option', 'Second option', 'Third option']);
});
it('extracts numbered list (1) ... 2) ...)', () => {
const text = '1) Alpha\n2) Beta';
expect(extractOptions(text)).toEqual(['Alpha', 'Beta']);
});
it('extracts lettered list (Latin)', () => {
const text = 'A. First choice\nB. Second choice\nC. Third choice';
expect(extractOptions(text)).toEqual(['First choice', 'Second choice', 'Third choice']);
});
it('extracts lettered list (Cyrillic)', () => {
const text = 'А. Первый\nБ. Второй\nВ. Третий';
expect(extractOptions(text)).toEqual(['Первый', 'Второй', 'Третий']);
});
it('extracts bullet list with hyphens', () => {
const text = '- Apple\n- Banana\n- Cherry';
expect(extractOptions(text)).toEqual(['Apple', 'Banana', 'Cherry']);
});
it('extracts bullet list with asterisks', () => {
const text = '* Red\n* Green\n* Blue';
expect(extractOptions(text)).toEqual(['Red', 'Green', 'Blue']);
});
it('extracts AskUserQuestion options from tool_use block', () => {
const askUser = {
type: 'tool_use',
name: 'AskUserQuestion',
input: {
questions: [{
options: [
{ label: 'Option A' },
{ label: 'Option B' },
{ label: 'Option C' },
],
}],
},
};
expect(extractOptions(askUser)).toEqual(['Option A', 'Option B', 'Option C']);
});
it('returns null when AskUserQuestion has < 2 options', () => {
const askUser = {
type: 'tool_use',
name: 'AskUserQuestion',
input: { questions: [{ options: [{ label: 'Only one' }] }] },
};
expect(extractOptions(askUser)).toBeNull();
});
});
describe('detectReference', () => {
const options = ['First option label', 'Second option label', 'Third option label'];
it('returns null on empty prompt', () => {
expect(detectReference('', options)).toBeNull();
expect(detectReference(' ', options)).toBeNull();
});
it('matches bare number at start', () => {
expect(detectReference('1', options)).toEqual({ index: 0, label: 'First option label' });
expect(detectReference('2 экономия 5%', options)).toEqual({ index: 1, label: 'Second option label' });
expect(detectReference('3, делаем', options)).toEqual({ index: 2, label: 'Third option label' });
});
it('matches verb-prefixed number', () => {
expect(detectReference('делай 1', options)).toEqual({ index: 0, label: 'First option label' });
expect(detectReference('выбираю 2', options)).toEqual({ index: 1, label: 'Second option label' });
expect(detectReference('беру 3', options)).toEqual({ index: 2, label: 'Third option label' });
expect(detectReference('хочу 1', options)).toEqual({ index: 0, label: 'First option label' });
expect(detectReference('вариант 2', options)).toEqual({ index: 1, label: 'Second option label' });
});
it('matches bare letter at start (Latin)', () => {
expect(detectReference('A', options)).toEqual({ index: 0, label: 'First option label' });
expect(detectReference('B делаем', options)).toEqual({ index: 1, label: 'Second option label' });
expect(detectReference('c, идём', options)).toEqual({ index: 2, label: 'Third option label' });
});
it('matches bare letter at start (Cyrillic)', () => {
expect(detectReference('А', options)).toEqual({ index: 0, label: 'First option label' });
expect(detectReference('б делаем', options)).toEqual({ index: 1, label: 'Second option label' });
expect(detectReference('в, идём', options)).toEqual({ index: 2, label: 'Third option label' });
});
it('matches verb-prefixed letter', () => {
expect(detectReference('делай A', options)).toEqual({ index: 0, label: 'First option label' });
expect(detectReference('выбираю Б', options)).toEqual({ index: 1, label: 'Second option label' });
});
it('matches label substring (first 2-4 words)', () => {
expect(detectReference('First option уточни', options)).toEqual({ index: 0, label: 'First option label' });
expect(detectReference('делаем Second option', options)).toEqual({ index: 1, label: 'Second option label' });
});
it('returns null when no signal matches', () => {
expect(detectReference('какой-то текст без ссылки', options)).toBeNull();
expect(detectReference('просто продолжай работу', options)).toBeNull();
});
it('returns null on out-of-range position', () => {
expect(detectReference('99', options)).toBeNull();
expect(detectReference('Z', options)).toBeNull();
expect(detectReference('Я', options)).toBeNull();
});
});
describe('detectChoiceProvenance', () => {
it('returns null when no options', () => {
expect(detectChoiceProvenance('1 делаем', 'just text without list')).toBeNull();
});
it('returns null when options exist but no reference', () => {
const lastAsst = '1. First\n2. Second\n3. Third';
expect(detectChoiceProvenance('расскажи как дела', lastAsst)).toBeNull();
});
it('returns full result on successful match (numbered)', () => {
const lastAsst = '1. Phase A\n2. Phase B\n3. Phase C';
expect(detectChoiceProvenance('2 делаем', lastAsst)).toEqual({
kind: 'user_chose_from_options',
node: 'Phase B',
options_offered: ['Phase A', 'Phase B', 'Phase C'],
claude_would_have_chosen: 'Phase A',
});
});
it('returns full result on successful match (Cyrillic letter, keyboard layout)', () => {
const lastAsst = 'А. Первый\nБ. Второй\nВ. Третий';
expect(detectChoiceProvenance('в делаем', lastAsst)).toEqual({
kind: 'user_chose_from_options',
node: 'Третий',
options_offered: ['Первый', 'Второй', 'Третий'],
claude_would_have_chosen: 'Первый',
});
});
it('returns full result with AskUserQuestion source', () => {
const askUser = {
type: 'tool_use',
name: 'AskUserQuestion',
input: { questions: [{ options: [{ label: 'Recommended' }, { label: 'Alternative' }] }] },
};
expect(detectChoiceProvenance('делай 2', askUser)).toEqual({
kind: 'user_chose_from_options',
node: 'Alternative',
options_offered: ['Recommended', 'Alternative'],
claude_would_have_chosen: 'Recommended',
});
});
});