Files
portal/tools/enforce-graph-first.test.mjs
T

210 lines
6.4 KiB
JavaScript
Raw Normal View History

import { describe, it, expect } from 'vitest';
import { decide } from './enforce-graph-first.mjs';
// Shared helpers
const GREP_TOOL = { name: 'Grep', input: { pattern: 'foo' } };
const GLOB_TOOL = { name: 'Glob', input: { pattern: '**/*.ts' } };
const READ_TOOL = { name: 'Read', input: { file_path: 'x.ts' } };
const EDIT_TOOL = { name: 'Edit', input: { file_path: 'x.mjs' } };
const BASH_TOOL = { name: 'Bash', input: { command: 'ls -la' } };
describe('enforce-graph-first / decide', () => {
// Test 1: No searches → pass
it('no searches at all → pass', () => {
expect(decide({
toolUses: [EDIT_TOOL],
graphifyInvoked: false,
assistantText: '',
override: null,
}).block).toBe(false);
});
// Test 2: Below threshold (2 searches) → pass
it('below threshold (2 Grep searches) → pass', () => {
expect(decide({
toolUses: [GREP_TOOL, GREP_TOOL],
graphifyInvoked: false,
assistantText: '',
override: null,
}).block).toBe(false);
});
// Test 3: 3 searches, no graphify, no override → block
it('3 Grep searches, no graphify, no override → block', () => {
const r = decide({
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
graphifyInvoked: false,
assistantText: '',
override: null,
});
expect(r.block).toBe(true);
expect(r.message).toMatch(/3/);
expect(r.message).toMatch(/graphify/i);
expect(r.message).toMatch(/graph-skip:/);
});
// Test 4: 5 searches but graphifyInvoked: true → pass
it('5 searches but graphifyInvoked: true → pass', () => {
expect(decide({
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL, GREP_TOOL, GREP_TOOL],
graphifyInvoked: true,
assistantText: '',
override: null,
}).block).toBe(false);
});
// Test 5: 3 searches with valid graph-skip line → pass
it('3 searches with valid graph-skip line → pass', () => {
expect(decide({
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
graphifyInvoked: false,
assistantText: 'graph-skip: узкий regex по литералу X\nдалее обычный ответ...',
override: null,
}).block).toBe(false);
});
// Test 6: 3 searches with empty graph-skip reason → block
it('3 searches with graph-skip: but empty reason → block', () => {
expect(decide({
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
graphifyInvoked: false,
assistantText: 'graph-skip:\n',
override: null,
}).block).toBe(true);
});
// Test 7: 3 searches with global override → pass
it('3 searches with global override → pass', () => {
expect(decide({
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
graphifyInvoked: false,
assistantText: '',
override: { phrase: 'срочно', suppresses: ['graph-first'] },
}).block).toBe(false);
});
// Test 8: Mixed Grep + Glob count toward threshold → block
it('1 Grep + 2 Glob = 3 → block (mixed counts toward threshold)', () => {
const r = decide({
toolUses: [GREP_TOOL, GLOB_TOOL, GLOB_TOOL],
graphifyInvoked: false,
assistantText: '',
override: null,
});
expect(r.block).toBe(true);
});
// Test 9: Other tools (Read, Edit, Bash) don't count as searches → pass
it('Read × 4 + Edit × 1 = 0 searches → pass', () => {
expect(decide({
toolUses: [READ_TOOL, READ_TOOL, READ_TOOL, READ_TOOL, EDIT_TOOL],
graphifyInvoked: false,
assistantText: '',
override: null,
}).block).toBe(false);
});
// Test 10: Message includes per-spec wording
it('block message includes §5 п.14, graphify, graph-skip: wording', () => {
const r = decide({
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
graphifyInvoked: false,
assistantText: '',
override: null,
});
expect(r.block).toBe(true);
expect(r.message).toMatch(/§5 п\.14/);
expect(r.message).toMatch(/graphify/i);
expect(r.message).toMatch(/graph-skip:/);
});
// Extra edge cases
it('exactly THRESHOLD=3 searches → block (boundary condition)', () => {
expect(decide({
toolUses: [GREP_TOOL, GLOB_TOOL, GREP_TOOL],
graphifyInvoked: false,
assistantText: '',
override: null,
}).block).toBe(true);
});
it('2 searches (below threshold) regardless of graphify state → pass', () => {
// Even without graphify, 2 searches is under the threshold
expect(decide({
toolUses: [GREP_TOOL, GLOB_TOOL],
graphifyInvoked: false,
assistantText: '',
override: null,
}).block).toBe(false);
});
it('graph-skip: with non-empty reason in middle of text → pass', () => {
const text = 'Some analysis first.\ngraph-skip: known file path, not cross-cutting\nThen conclusion.';
expect(decide({
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
graphifyInvoked: false,
assistantText: text,
override: null,
}).block).toBe(false);
});
it('graph-skip: with only whitespace reason (not \\ S+) → block', () => {
expect(decide({
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
graphifyInvoked: false,
assistantText: 'graph-skip: \n',
override: null,
}).block).toBe(true);
});
it('empty toolUses → pass', () => {
expect(decide({
toolUses: [],
graphifyInvoked: false,
assistantText: '',
override: null,
}).block).toBe(false);
});
it('Bash tool alone does not count as search', () => {
expect(decide({
toolUses: [BASH_TOOL, BASH_TOOL, BASH_TOOL, BASH_TOOL],
graphifyInvoked: false,
assistantText: '',
override: null,
}).block).toBe(false);
});
it('block message includes the actual count N', () => {
const r = decide({
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL, GREP_TOOL, GREP_TOOL],
graphifyInvoked: false,
assistantText: '',
override: null,
});
expect(r.block).toBe(true);
expect(r.message).toMatch(/5/);
});
it('override null value → treated as falsy, block still fires', () => {
const r = decide({
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
graphifyInvoked: false,
assistantText: '',
override: null,
});
expect(r.block).toBe(true);
});
it('override false value → treated as falsy, block still fires', () => {
const r = decide({
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
graphifyInvoked: false,
assistantText: '',
override: false,
});
expect(r.block).toBe(true);
});
});