This commit is contained in:
271
packages/agent/tests/capability-acquisition-trust.test.ts
Normal file
271
packages/agent/tests/capability-acquisition-trust.test.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { searchCapabilities } from '../src/capability-acquisition.js';
|
||||
|
||||
describe('capability-acquisition trust integration', () => {
|
||||
let tmpDir: string;
|
||||
let starterDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-trust-'));
|
||||
starterDir = path.join(tmpDir, 'starter-skills');
|
||||
fs.mkdirSync(starterDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function writeStarter(name: string, content: string) {
|
||||
fs.writeFileSync(path.join(starterDir, `${name}.md`), content);
|
||||
}
|
||||
|
||||
it('candidates from searchCapabilities carry trust assessments', () => {
|
||||
writeStarter('risk-assessment', '# Risk Assessment\n\n1. Identify risks\n2. Evaluate\n3. Mitigate');
|
||||
|
||||
const proposal = searchCapabilities({
|
||||
need: 'risk assessment for my project',
|
||||
installedSkills: [],
|
||||
starterSkillsDir: starterDir,
|
||||
nativeToolNames: [],
|
||||
});
|
||||
|
||||
const candidate = proposal.candidates.find(c => c.name === 'risk-assessment');
|
||||
expect(candidate).toBeDefined();
|
||||
expect(candidate!.trust).toBeDefined();
|
||||
expect(candidate!.trust!.trustSource).toBe('starter_pack');
|
||||
expect(candidate!.trust!.riskLevel).toBe('low');
|
||||
expect(candidate!.trust!.assessmentMode).toBe('heuristic');
|
||||
});
|
||||
|
||||
it('native tool candidates get builtin trust source', () => {
|
||||
const proposal = searchCapabilities({
|
||||
need: 'search the web for information',
|
||||
installedSkills: [],
|
||||
starterSkillsDir: starterDir,
|
||||
nativeToolNames: ['web_search', 'web_fetch'],
|
||||
});
|
||||
|
||||
const webSearch = proposal.candidates.find(c => c.name === 'web_search');
|
||||
expect(webSearch).toBeDefined();
|
||||
expect(webSearch!.trust).toBeDefined();
|
||||
expect(webSearch!.trust!.trustSource).toBe('builtin');
|
||||
expect(webSearch!.trust!.riskLevel).toBe('low');
|
||||
});
|
||||
|
||||
it('installed skill candidates get local_user trust source', () => {
|
||||
const proposal = searchCapabilities({
|
||||
need: 'review my code',
|
||||
installedSkills: [
|
||||
{ name: 'code-review', content: '# Code Review\n\nCheck for bugs and style issues.' },
|
||||
],
|
||||
starterSkillsDir: starterDir,
|
||||
nativeToolNames: [],
|
||||
});
|
||||
|
||||
const codeReview = proposal.candidates.find(c => c.name === 'code-review');
|
||||
expect(codeReview).toBeDefined();
|
||||
expect(codeReview!.trust!.trustSource).toBe('local_user');
|
||||
});
|
||||
|
||||
it('proposal summary includes risk level for installable candidates', () => {
|
||||
writeStarter('research-synthesis', '# Research Synthesis\n\nSynthesize research findings from web_search and web_fetch results');
|
||||
|
||||
const proposal = searchCapabilities({
|
||||
need: 'synthesize research',
|
||||
installedSkills: [],
|
||||
starterSkillsDir: starterDir,
|
||||
nativeToolNames: [],
|
||||
});
|
||||
|
||||
expect(proposal.gapDetected).toBe(true);
|
||||
// Summary should mention risk
|
||||
expect(proposal.summary).toContain('Risk level');
|
||||
});
|
||||
|
||||
it('high-risk starter skill is correctly classified', () => {
|
||||
writeStarter('deploy-tool', '# Deploy Tool\n\nUse bash to deploy with the api_key, then write_file the deployment log');
|
||||
|
||||
const proposal = searchCapabilities({
|
||||
need: 'deploy my application',
|
||||
installedSkills: [],
|
||||
starterSkillsDir: starterDir,
|
||||
nativeToolNames: [],
|
||||
});
|
||||
|
||||
const deployer = proposal.candidates.find(c => c.name === 'deploy-tool');
|
||||
expect(deployer).toBeDefined();
|
||||
// starter_pack base=0 + codeExecution=2 + secrets=2 + fileSystem=1 = 5 → high
|
||||
expect(deployer!.trust!.riskLevel).toBe('high');
|
||||
expect(deployer!.trust!.approvalClass).toBe('critical');
|
||||
});
|
||||
|
||||
it('low-risk instruction-only skill has no elevated permissions', () => {
|
||||
writeStarter('brainstorm', '# Brainstorm\n\nUse divergent thinking, then converge on ideas.');
|
||||
|
||||
const proposal = searchCapabilities({
|
||||
need: 'brainstorm ideas',
|
||||
installedSkills: [],
|
||||
starterSkillsDir: starterDir,
|
||||
nativeToolNames: [],
|
||||
});
|
||||
|
||||
const brainstorm = proposal.candidates.find(c => c.name === 'brainstorm');
|
||||
expect(brainstorm).toBeDefined();
|
||||
const perms = brainstorm!.trust!.permissions;
|
||||
expect(perms.fileSystem).toBe(false);
|
||||
expect(perms.network).toBe(false);
|
||||
expect(perms.codeExecution).toBe(false);
|
||||
expect(perms.externalServices).toBe(false);
|
||||
expect(perms.secrets).toBe(false);
|
||||
expect(perms.browserAutomation).toBe(false);
|
||||
});
|
||||
|
||||
it('trust assessment does not change candidate scoring/ranking', () => {
|
||||
writeStarter('risk-assessment', '# Risk Assessment\n\nEvaluate project risks');
|
||||
writeStarter('risk-matrix', '# Risk Matrix\n\nCreate a risk probability/impact matrix');
|
||||
|
||||
const proposal = searchCapabilities({
|
||||
need: 'risk assessment',
|
||||
installedSkills: [],
|
||||
starterSkillsDir: starterDir,
|
||||
nativeToolNames: [],
|
||||
});
|
||||
|
||||
// Both should appear, scored by keyword match, not by trust
|
||||
expect(proposal.candidates.length).toBeGreaterThanOrEqual(1);
|
||||
// Scoring is by keyword match — risk-assessment has direct name match
|
||||
const first = proposal.candidates[0];
|
||||
expect(first.name).toBe('risk-assessment');
|
||||
expect(first.trust).toBeDefined();
|
||||
});
|
||||
|
||||
it('proposal with installable recommendation mentions approval class', () => {
|
||||
writeStarter('daily-plan', '# Daily Plan\n\nOrganize your day effectively');
|
||||
|
||||
const proposal = searchCapabilities({
|
||||
need: 'plan my day',
|
||||
installedSkills: [],
|
||||
starterSkillsDir: starterDir,
|
||||
nativeToolNames: [],
|
||||
});
|
||||
|
||||
if (proposal.recommendation?.availability === 'installable') {
|
||||
expect(proposal.summary).toContain('Approval required');
|
||||
}
|
||||
});
|
||||
|
||||
it('candidate trust is available for runtime consumption', () => {
|
||||
writeStarter('meeting-prep', '# Meeting Prep\n\nPrepare talking points and agenda');
|
||||
|
||||
const proposal = searchCapabilities({
|
||||
need: 'prepare for meeting',
|
||||
installedSkills: [],
|
||||
starterSkillsDir: starterDir,
|
||||
nativeToolNames: [],
|
||||
});
|
||||
|
||||
const candidate = proposal.candidates.find(c => c.name === 'meeting-prep');
|
||||
expect(candidate).toBeDefined();
|
||||
// Trust assessment is structured for runtime use
|
||||
const trust = candidate!.trust!;
|
||||
expect(typeof trust.riskLevel).toBe('string');
|
||||
expect(typeof trust.trustSource).toBe('string');
|
||||
expect(typeof trust.approvalClass).toBe('string');
|
||||
expect(typeof trust.assessmentMode).toBe('string');
|
||||
expect(typeof trust.explanation).toBe('string');
|
||||
expect(Array.isArray(trust.factors)).toBe(true);
|
||||
expect(typeof trust.permissions).toBe('object');
|
||||
});
|
||||
|
||||
it('skill with declared permissions gets assessmentMode mixed', () => {
|
||||
const skillWithFrontmatter = `---
|
||||
permissions:
|
||||
network: true
|
||||
codeExecution: true
|
||||
---
|
||||
|
||||
# Deploy Skill
|
||||
Deploy using web_fetch and bash commands.`;
|
||||
|
||||
writeStarter('deploy-skill', skillWithFrontmatter);
|
||||
|
||||
const proposal = searchCapabilities({
|
||||
need: 'deploy my application',
|
||||
installedSkills: [],
|
||||
starterSkillsDir: starterDir,
|
||||
nativeToolNames: [],
|
||||
});
|
||||
|
||||
const deployer = proposal.candidates.find(c => c.name === 'deploy-skill');
|
||||
expect(deployer).toBeDefined();
|
||||
expect(deployer!.trust).toBeDefined();
|
||||
// Has both declared permissions AND content analysis -> mixed
|
||||
expect(deployer!.trust!.assessmentMode).toBe('mixed');
|
||||
expect(deployer!.trust!.permissions.network).toBe(true);
|
||||
expect(deployer!.trust!.permissions.codeExecution).toBe(true);
|
||||
});
|
||||
|
||||
it('skill without frontmatter still gets assessmentMode heuristic', () => {
|
||||
writeStarter('plain-skill', '# Plain Skill\n\nJust instructions, no frontmatter at all.');
|
||||
|
||||
const proposal = searchCapabilities({
|
||||
need: 'plain skill for task',
|
||||
installedSkills: [],
|
||||
starterSkillsDir: starterDir,
|
||||
nativeToolNames: [],
|
||||
});
|
||||
|
||||
const plain = proposal.candidates.find(c => c.name === 'plain-skill');
|
||||
expect(plain).toBeDefined();
|
||||
expect(plain!.trust!.assessmentMode).toBe('heuristic');
|
||||
});
|
||||
|
||||
it('declared permissions are merged with heuristic detection', () => {
|
||||
// Frontmatter declares network, content mentions bash (codeExecution heuristic)
|
||||
const skillContent = `---
|
||||
permissions:
|
||||
network: true
|
||||
---
|
||||
|
||||
# Hybrid Skill
|
||||
Run bash commands to process data.`;
|
||||
|
||||
writeStarter('hybrid-skill', skillContent);
|
||||
|
||||
const proposal = searchCapabilities({
|
||||
need: 'hybrid skill processing',
|
||||
installedSkills: [],
|
||||
starterSkillsDir: starterDir,
|
||||
nativeToolNames: [],
|
||||
});
|
||||
|
||||
const hybrid = proposal.candidates.find(c => c.name === 'hybrid-skill');
|
||||
expect(hybrid).toBeDefined();
|
||||
// network from declared, codeExecution from heuristic (bash keyword)
|
||||
expect(hybrid!.trust!.permissions.network).toBe(true);
|
||||
expect(hybrid!.trust!.permissions.codeExecution).toBe(true);
|
||||
expect(hybrid!.trust!.assessmentMode).toBe('mixed');
|
||||
});
|
||||
|
||||
it('multiple candidates each have independent trust assessments', () => {
|
||||
writeStarter('simple-skill', '# Simple\n\nJust instructions, no tools.');
|
||||
writeStarter('complex-skill', '# Complex\n\nUse bash and web_fetch with api_key');
|
||||
|
||||
const proposal = searchCapabilities({
|
||||
need: 'skill for my task',
|
||||
installedSkills: [],
|
||||
starterSkillsDir: starterDir,
|
||||
nativeToolNames: ['bash'],
|
||||
});
|
||||
|
||||
const simple = proposal.candidates.find(c => c.name === 'simple-skill');
|
||||
const complex = proposal.candidates.find(c => c.name === 'complex-skill');
|
||||
|
||||
if (simple && complex) {
|
||||
expect(simple.trust!.riskLevel).not.toBe(complex.trust!.riskLevel);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user