Files
promptfoo--promptfoo/test/assertions/skill.test.ts
T
wehub-resource-sync 0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

241 lines
6.4 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { runAssertion } from '../../src/assertions/index';
import type { Assertion, AtomicTestCase, ProviderResponse } from '../../src/types/index';
describe('skill-used assertion', () => {
const testCase: AtomicTestCase = {
vars: {},
};
const providerResponse: ProviderResponse = {
output: 'Done',
metadata: {
skillCalls: [
{
name: 'token-skill',
path: '.agents/skills/token-skill/SKILL.md',
source: 'heuristic',
},
{
name: 'project-standards:standards-check',
source: 'tool',
},
],
},
};
async function runSkillAssertion(assertion: Assertion) {
return runAssertion({
assertion,
test: testCase,
providerResponse,
});
}
it('passes when an exact skill name is present', async () => {
const result = await runSkillAssertion({
type: 'skill-used',
value: 'token-skill',
});
expect(result.pass).toBe(true);
expect(result.reason).toContain('Observed required skill(s): token-skill');
});
it('ignores errored skill calls when checking skill-used assertions', async () => {
const result = await runAssertion({
assertion: {
type: 'skill-used',
value: 'missing-skill',
},
test: testCase,
providerResponse: {
output: 'Done',
metadata: {
skillCalls: [
{
name: 'missing-skill',
source: 'tool',
is_error: true,
},
],
},
},
});
expect(result.pass).toBe(false);
expect(result.reason).toContain('Missing required skill(s): missing-skill');
expect(result.reason).toContain('Actual skills: (none)');
});
it('passes when all expected skills in a list are present', async () => {
const result = await runSkillAssertion({
type: 'skill-used',
value: ['token-skill', 'project-standards:standards-check'],
});
expect(result.pass).toBe(true);
});
it('supports pattern matching with count thresholds', async () => {
const result = await runSkillAssertion({
type: 'skill-used',
value: {
pattern: 'project-*:*',
min: 1,
max: 1,
},
});
expect(result.pass).toBe(true);
expect(result.reason).toContain('Matched skill "project-*:*" 1 time(s)');
});
it('trims object matcher name and pattern values', async () => {
const result = await runSkillAssertion({
type: 'skill-used',
value: {
pattern: ' project-*:* ',
min: 1,
},
});
expect(result.pass).toBe(true);
expect(result.reason).toContain('Matched skill "project-*:*" 1 time(s)');
});
it('supports inverse assertions', async () => {
const result = await runSkillAssertion({
type: 'not-skill-used',
value: 'forbidden-skill',
});
expect(result.pass).toBe(true);
});
it('fails when a required skill is missing', async () => {
const result = await runSkillAssertion({
type: 'skill-used',
value: 'missing-skill',
});
expect(result.pass).toBe(false);
expect(result.reason).toContain('Missing required skill(s): missing-skill');
});
it('fails inverse assertions when a forbidden skill is used', async () => {
const result = await runSkillAssertion({
type: 'not-skill-used',
value: 'token-skill',
});
expect(result.pass).toBe(false);
expect(result.reason).toContain('Forbidden skill(s) were used: token-skill');
});
it('treats not-skill-used object assertions with no count bounds as forbidding any match', async () => {
const result = await runSkillAssertion({
type: 'not-skill-used',
value: {
pattern: 'token-*',
},
});
expect(result.pass).toBe(false);
expect(result.reason).toContain('Forbidden skill "token-*" was used 1 time(s)');
});
it('fails not-skill-used object assertions with max: 0 when a matching skill is present', async () => {
const result = await runSkillAssertion({
type: 'not-skill-used',
value: {
pattern: 'token-*',
max: 0,
},
});
expect(result.pass).toBe(false);
expect(result.reason).toContain('Forbidden skill "token-*" was used 1 time(s)');
});
it('passes not-skill-used object assertions with max: 0 when no matching skill is present', async () => {
const result = await runSkillAssertion({
type: 'not-skill-used',
value: {
pattern: 'missing-*',
max: 0,
},
});
expect(result.pass).toBe(true);
expect(result.reason).toContain('Forbidden skill "missing-*" was not used');
});
it('rejects ambiguous not-skill-used count bounds other than max: 0', async () => {
await expect(
runSkillAssertion({
type: 'not-skill-used',
value: {
pattern: 'token-*',
max: 1,
},
}),
).rejects.toThrow(
'not-skill-used object assertions only support name/pattern with no count bounds, or max: 0',
);
});
it('rejects invalid count bounds', async () => {
await expect(
runSkillAssertion({
type: 'skill-used',
value: {
pattern: 'token-*',
min: -1,
},
}),
).rejects.toThrow('skill-used assertion object min must be a finite non-negative integer');
await expect(
runSkillAssertion({
type: 'skill-used',
value: {
pattern: 'token-*',
min: 2,
max: 1,
},
}),
).rejects.toThrow('skill-used assertion object max must be greater than or equal to min');
});
it('throws when object values omit name and pattern', async () => {
await expect(
runSkillAssertion({
type: 'skill-used',
value: { min: 1 },
}),
).rejects.toThrow('skill-used assertion object must include a name or pattern property');
});
it('fails gracefully when skillCalls is an empty array', async () => {
const result = await runAssertion({
assertion: {
type: 'skill-used',
value: 'token-skill',
},
test: testCase,
providerResponse: {
output: 'Done',
metadata: {
skillCalls: [],
},
},
});
expect(result.pass).toBe(false);
expect(result.reason).toContain('Missing required skill(s): token-skill');
expect(result.reason).toContain('Actual skills: (none)');
});
});