Files
proffesor-for-testing__agen…/tests/unit/learning/skill-validation-learner.test.ts
Dragan Spiridonov 4a641e7ef1 fix(adr-093): post-audit sweep — CI workflow, consensus provider, MCP sampling, 12 skill evals, 9 test files
Fifth post-audit sweep. 24 files, +66/-60.

CRITICAL (user-facing):
* .github/workflows/skill-validation.yml:422: CI eval runner
  --model claude-sonnet-4 → claude-sonnet-4-6. Previously CI would
  target a retiring model.
* src/coordination/consensus/providers/claude-provider.ts: Claude 3.x
  consensus provider modernized — type union extended with
  claude-sonnet-4-6 / claude-opus-4-7 / claude-haiku-4-5 as first
  entries, default model updated from claude-3-5-sonnet-20241022
  to claude-sonnet-4-6, availableModels list prepends new models.
  Legacy 3.x entries retained for backward compat.
* src/mcp/security/sampling-server.ts: default model
  claude-3-sonnet-20240229 (retired 2026-02-19) → claude-sonnet-4-6.

Skill evals (12 files, 6 + 6 mirrors):
* .claude/skills/ and assets/skills/ mirrors for:
  performance-testing, chaos-engineering-resilience, quality-metrics,
  localization-testing, accessibility-testing,
  n8n-trigger-testing-strategies. models_to_test list updated to
  claude-sonnet-4-6 / claude-haiku-4-5.

Test files (9):
* tests/unit/coordination/consensus/providers/claude-provider.test.ts:
  default model, test descriptions, and references updated to 4.6/4.7.
* tests/unit/routing/economic-routing.test.ts: claude-3-opus-20240229
  usage → claude-opus-4-7.
* tests/unit/learning/skill-validation-learner.test.ts: claude-3-haiku
  / claude-3.5-sonnet / claude-3-opus → new canonicals.
* tests/unit/mcp/handlers/handler-test-utils.ts: claude-3-haiku →
  claude-haiku-4-5.
* tests/integration/llm/{real-provider-integration,multi-provider}.test.ts:
  bare 'claude-sonnet-4' canonical → 'claude-sonnet-4-6'.
* tests/integration/validation/parallel-eval-runner.test.ts:
  claude-3.5-sonnet / claude-3-haiku → new canonicals.
* tests/routing/advisor/multi-model-executor.test.ts:
  CRITICAL BUG — 'claude-opus-4-6' (non-existent model ID, typo) ×2
  → 'claude-opus-4-7'. Stale test name 'anthropic/claude-opus-4
  when no provider' → '-4.7'.
* tests/unit/opencode/degradation.test.ts: CRITICAL BUG —
  modelId 'claude-opus-4-6' (typo) → 'claude-opus-4-7'.

Left intentionally untouched:
* tests/unit/coordination/consensus/providers/openrouter-provider.test.ts:
  tests that OpenRouter provider lists claude-3-haiku / claude-3.5-sonnet
  / claude-3-opus as supported. These assertions reflect actual
  OpenRouter provider data, which hasn't changed. Adding new-model
  assertions would be an enhancement, not a fix.

Verification:
* 0 tsc errors
* 174/174 tests across touched paths pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:26:23 +00:00

300 lines
9.6 KiB
TypeScript

/**
* Skill Validation Learner Tests
* ADR-056: Integrates skill validation with ReasoningBank
*/
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import {
SkillValidationLearner,
createSkillValidationLearner,
type SkillValidationOutcome,
type TestCaseResult,
} from '../../../src/learning/skill-validation-learner.js';
import type { RealQEReasoningBank } from '../../../src/learning/real-qe-reasoning-bank.js';
// Mock ReasoningBank
function createMockReasoningBank(): RealQEReasoningBank {
const storedPatterns = new Map<string, any>();
return {
storeQEPattern: vi.fn(async (options) => {
const id = `pattern-${Date.now()}-${Math.random().toString(36).slice(2)}`;
storedPatterns.set(options.name, {
id,
...options,
context: options.context || { tags: [] },
template: options.template || { type: 'data', content: '{}', variables: [] },
});
return { success: true, value: { id, ...options } };
}),
searchQEPatterns: vi.fn(async (query, _options) => {
// Find patterns that match the query in name
const matches: any[] = [];
for (const [name, pattern] of storedPatterns) {
if (name.includes(query) || query.includes(name.split('-')[0])) {
matches.push({ pattern, similarity: 0.9 });
}
}
return { success: true, value: matches };
}),
queryPatterns: vi.fn(async () => []),
} as unknown as RealQEReasoningBank;
}
describe('SkillValidationLearner', () => {
let learner: SkillValidationLearner;
let mockReasoningBank: RealQEReasoningBank;
beforeEach(() => {
mockReasoningBank = createMockReasoningBank();
learner = createSkillValidationLearner(mockReasoningBank);
});
afterEach(() => {
vi.clearAllMocks();
});
describe('recordValidationOutcome', () => {
it('should store a validation pattern in ReasoningBank', async () => {
const outcome: SkillValidationOutcome = {
skillName: 'security-testing',
trustTier: 3,
validationLevel: 'eval',
model: 'claude-sonnet-4-6',
passed: true,
score: 0.92,
testCaseResults: [
{
testId: 'test-1',
passed: true,
expectedPatterns: ['SQL injection'],
actualPatterns: ['SQL injection', 'vulnerability'],
reasoningQuality: 0.95,
},
],
timestamp: new Date(),
runId: 'run-123',
};
await learner.recordValidationOutcome(outcome);
expect(mockReasoningBank.storeQEPattern).toHaveBeenCalled();
// Check the pattern was stored with correct data
const calls = (mockReasoningBank.storeQEPattern as any).mock.calls;
expect(calls.length).toBeGreaterThan(0);
const firstCall = calls[0][0];
expect(firstCall.name).toContain('security-testing');
expect(firstCall.context.tags).toContain('skill-validation');
});
it('should update skill confidence on each outcome', async () => {
// Record multiple outcomes
for (let i = 0; i < 5; i++) {
await learner.recordValidationOutcome({
skillName: 'a11y-testing',
trustTier: 2,
validationLevel: 'validator',
model: 'claude-haiku-4-5',
passed: i % 2 === 0,
score: 0.7 + (i * 0.05),
testCaseResults: [
{
testId: `test-${i}`,
passed: i % 2 === 0,
expectedPatterns: [],
actualPatterns: [],
reasoningQuality: 0.8,
},
],
timestamp: new Date(),
});
}
// Confidence should be stored
const calls = (mockReasoningBank.storeQEPattern as any).mock.calls;
const confidenceCalls = calls.filter((c: any) =>
c[0].name.includes('skill-confidence')
);
expect(confidenceCalls.length).toBeGreaterThan(0);
});
it('should track cross-model behavior for eval level', async () => {
// Record outcomes from different models
const models = ['claude-sonnet-4-6', 'claude-haiku-4-5', 'claude-opus-4-7'];
for (const model of models) {
await learner.recordValidationOutcome({
skillName: 'security-testing',
trustTier: 3,
validationLevel: 'eval',
model,
passed: true,
score: 0.9,
testCaseResults: [],
timestamp: new Date(),
});
}
// Cross-model tracking should be stored
const calls = (mockReasoningBank.storeQEPattern as any).mock.calls;
const crossModelCalls = calls.filter((c: any) =>
c[0].name.includes('cross-model')
);
expect(crossModelCalls.length).toBeGreaterThan(0);
});
it('should not track cross-model for schema/validator levels', async () => {
await learner.recordValidationOutcome({
skillName: 'test-skill',
trustTier: 1,
validationLevel: 'schema',
model: 'claude-haiku-4-5',
passed: true,
score: 1.0,
testCaseResults: [],
timestamp: new Date(),
});
const calls = (mockReasoningBank.storeQEPattern as any).mock.calls;
const crossModelCalls = calls.filter((c: any) =>
c[0].name.includes('cross-model')
);
expect(crossModelCalls.length).toBe(0);
});
});
describe('getSkillConfidence', () => {
it('should return null for unknown skill', async () => {
const confidence = await learner.getSkillConfidence('unknown-skill');
expect(confidence).toBeNull();
});
it('should return confidence data after recording outcomes', async () => {
// Record an outcome first
await learner.recordValidationOutcome({
skillName: 'test-skill',
trustTier: 2,
validationLevel: 'validator',
model: 'claude-sonnet-4-6',
passed: true,
score: 0.85,
testCaseResults: [],
timestamp: new Date(),
});
// The mock should return the stored pattern
const confidence = await learner.getSkillConfidence('test-skill');
// With our mock, this will return the stored confidence
// In real implementation, this would parse the pattern content
});
});
describe('getCrossModelAnalysis', () => {
it('should return null for unknown skill', async () => {
const analysis = await learner.getCrossModelAnalysis('unknown-skill');
expect(analysis).toBeNull();
});
});
describe('queryValidationPatterns', () => {
it('should return validation patterns for a skill', async () => {
// Record some outcomes first
await learner.recordValidationOutcome({
skillName: 'security-testing',
trustTier: 3,
validationLevel: 'eval',
model: 'claude-sonnet-4-6',
passed: true,
score: 0.9,
testCaseResults: [],
timestamp: new Date(),
});
const patterns = await learner.queryValidationPatterns('security-testing');
// Should return patterns that match the skill
expect(Array.isArray(patterns)).toBe(true);
});
});
describe('getValidationTrends', () => {
it('should return null for unknown skill', async () => {
const trends = await learner.getValidationTrends('unknown-skill');
expect(trends).toBeNull();
});
});
describe('extractLearnedPatterns', () => {
it('should extract patterns from validation history', async () => {
// Record multiple outcomes
for (let i = 0; i < 3; i++) {
await learner.recordValidationOutcome({
skillName: 'security-testing',
trustTier: 3,
validationLevel: 'eval',
model: 'claude-sonnet-4-6',
passed: i < 2,
score: i < 2 ? 0.9 : 0.6,
testCaseResults: [
{
testId: `test-${i}`,
passed: i < 2,
expectedPatterns: ['SQL injection'],
actualPatterns: ['SQL injection'],
reasoningQuality: 0.8,
category: 'injection',
},
],
timestamp: new Date(),
metadata: { category: 'injection' },
});
}
const patterns = await learner.extractLearnedPatterns('security-testing');
expect(Array.isArray(patterns)).toBe(true);
});
});
describe('connectFeedbackLoop', () => {
it('should connect to QualityFeedbackLoop', () => {
const mockFeedbackLoop = {
recordRoutingOutcome: vi.fn(),
} as any;
// Should not throw
learner.connectFeedbackLoop(mockFeedbackLoop);
});
it('should record to feedback loop when connected and runId is present', async () => {
const mockFeedbackLoop = {
recordRoutingOutcome: vi.fn(),
} as any;
learner.connectFeedbackLoop(mockFeedbackLoop);
await learner.recordValidationOutcome({
skillName: 'test-skill',
trustTier: 2,
validationLevel: 'eval',
model: 'claude-sonnet-4-6',
passed: true,
score: 0.9,
testCaseResults: [],
timestamp: new Date(),
runId: 'run-123',
});
expect(mockFeedbackLoop.recordRoutingOutcome).toHaveBeenCalled();
});
});
});
describe('createSkillValidationLearner', () => {
it('should create a SkillValidationLearner instance', () => {
const mockBank = createMockReasoningBank();
const learner = createSkillValidationLearner(mockBank);
expect(learner).toBeInstanceOf(SkillValidationLearner);
});
});