feat: implement tier 1 quick wins (tool groups, typing, pruning, verbose, think)
Five additive features with no breaking changes: - Tool groups: group:fs, group:runtime, group:web, group:memory syntactic sugar for allow/deny lists in tool policy config - Typing indicators: Discord sendTyping() and WhatsApp sendStateTyping() on message receipt for better UX feedback - Session pruning: TTL-based auto-cleanup via sessions.ttl config with hourly daemon timer and SQLite GROUP BY pruning - /verbose command: TUI command parser toggle for raw streaming display - !!think prefix: per-message extended thinking mode wired through Anthropic (budget_tokens), OpenAI/GitHub (reasoning_effort), and Gemini (thinkingConfig) providers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -409,6 +409,90 @@ describe('ToolPolicy', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('tool groups', () => {
|
||||
it('expands group:fs in allow list', () => {
|
||||
const policy = new ToolPolicy(defaultConfig({
|
||||
profile: 'minimal',
|
||||
allow: ['group:fs'],
|
||||
}));
|
||||
const result = policy.filterTools(ALL_TOOLS);
|
||||
const names = result.map(t => t.name);
|
||||
expect(names).toContain('file.read');
|
||||
expect(names).toContain('file.write');
|
||||
expect(names).toContain('file.edit');
|
||||
expect(names).toContain('file.list');
|
||||
expect(names).not.toContain('shell.exec');
|
||||
});
|
||||
|
||||
it('expands group:runtime in deny list', () => {
|
||||
const policy = new ToolPolicy(defaultConfig({
|
||||
deny: ['group:runtime'],
|
||||
}));
|
||||
const result = policy.filterTools(ALL_TOOLS);
|
||||
const names = result.map(t => t.name);
|
||||
expect(names).not.toContain('shell.exec');
|
||||
expect(names).not.toContain('process.start');
|
||||
expect(names).not.toContain('process.status');
|
||||
expect(names).not.toContain('process.output');
|
||||
expect(names).not.toContain('process.kill');
|
||||
expect(names).not.toContain('process.list');
|
||||
expect(names).toContain('file.read');
|
||||
});
|
||||
|
||||
it('expands groups in agent overrides', () => {
|
||||
const policy = new ToolPolicy(defaultConfig({
|
||||
agents: {
|
||||
fast: { profile: 'minimal', allow: ['group:memory'], deny: [] },
|
||||
},
|
||||
}));
|
||||
const result = policy.filterTools(ALL_TOOLS, { agent: 'fast' });
|
||||
const names = result.map(t => t.name);
|
||||
expect(names).toContain('memory.read');
|
||||
expect(names).toContain('memory.write');
|
||||
expect(names).toContain('memory.search');
|
||||
expect(names).toContain('file.read'); // from minimal profile
|
||||
expect(names).not.toContain('shell.exec');
|
||||
});
|
||||
|
||||
it('expands groups in provider deny', () => {
|
||||
const policy = new ToolPolicy(defaultConfig({
|
||||
providers: {
|
||||
ollama: { allow: [], deny: ['group:web'] },
|
||||
},
|
||||
}));
|
||||
const result = policy.filterTools(ALL_TOOLS, { provider: 'ollama' });
|
||||
const names = result.map(t => t.name);
|
||||
expect(names).not.toContain('web.fetch');
|
||||
expect(names).not.toContain('web.search');
|
||||
expect(names).toContain('file.read');
|
||||
expect(names).toContain('shell.exec');
|
||||
});
|
||||
|
||||
it('mixes groups with individual names', () => {
|
||||
const policy = new ToolPolicy(defaultConfig({
|
||||
profile: 'minimal',
|
||||
allow: ['group:memory', 'shell.exec'],
|
||||
}));
|
||||
const result = policy.filterTools(ALL_TOOLS);
|
||||
const names = result.map(t => t.name);
|
||||
expect(names).toContain('memory.read');
|
||||
expect(names).toContain('shell.exec');
|
||||
expect(names).toContain('file.read'); // from minimal
|
||||
});
|
||||
|
||||
it('unknown group name passes through as literal', () => {
|
||||
const policy = new ToolPolicy(defaultConfig({
|
||||
profile: 'minimal',
|
||||
allow: ['group:nonexistent'],
|
||||
}));
|
||||
const result = policy.filterTools(ALL_TOOLS);
|
||||
// Should only have minimal tools — 'group:nonexistent' doesn't match any real tool
|
||||
const names = result.map(t => t.name);
|
||||
expect(names).toContain('file.read');
|
||||
expect(names).not.toContain('shell.exec');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles empty tool list', () => {
|
||||
const policy = new ToolPolicy(defaultConfig());
|
||||
|
||||
Reference in New Issue
Block a user