moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,238 @@
/**
* Tests for prompt-shapes layer (Phase 1.2 of agent-fix sprint).
*
* Coverage:
* - Selector: exact match, prefix match, default fallback, override
* - Each shape: 4 methods return non-empty strings
* - Each shape: metadata.evidence_link is non-empty
* - All shapes registered + listed
* - Throw on unknown override
* - System prompt distinguishes solo vs multi-step
*/
import { describe, it, expect } from 'vitest';
import {
selectShape,
listShapes,
getShapeMetadata,
REGISTRY,
claudeShape,
qwenThinkingShape,
qwenNonThinkingShape,
gptShape,
genericSimpleShape,
MULTI_STEP_ACTION_CONTRACT,
_resetConfigCache,
} from '../src/prompt-shapes/index.js';
describe('selector — alias resolution', () => {
it('exact match: claude-opus-4-7 → claude', () => {
const shape = selectShape('claude-opus-4-7');
expect(shape.name).toBe('claude');
});
it('exact match: qwen3.6-35b-a3b-via-dashscope-direct → qwen-thinking', () => {
const shape = selectShape('qwen3.6-35b-a3b-via-dashscope-direct');
expect(shape.name).toBe('qwen-thinking');
});
it('exact match: gpt-5.4 → gpt', () => {
const shape = selectShape('gpt-5.4');
expect(shape.name).toBe('gpt');
});
it('prefix match: claude-haiku-4-5-20251001 → claude (via claude- prefix)', () => {
const shape = selectShape('claude-haiku-4-5-20251001');
expect(shape.name).toBe('claude');
});
it('prefix match: minimax-m27-via-openrouter → generic-simple', () => {
const shape = selectShape('minimax-m27-via-openrouter');
expect(shape.name).toBe('generic-simple');
});
it('prefix match: gpt-6 → gpt (via gpt- prefix)', () => {
const shape = selectShape('gpt-6');
expect(shape.name).toBe('gpt');
});
it('default fallback: completely unknown alias → generic-simple', () => {
const shape = selectShape('mistral-7b-some-future-alias');
expect(shape.name).toBe('generic-simple');
});
it('default fallback: empty alias → generic-simple', () => {
const shape = selectShape('');
expect(shape.name).toBe('generic-simple');
});
});
describe('selector — override', () => {
it('override: claude-opus-4-7 with override="generic-simple" → generic-simple', () => {
const shape = selectShape('claude-opus-4-7', { override: 'generic-simple' });
expect(shape.name).toBe('generic-simple');
});
it('override: any alias with override="qwen-non-thinking" → qwen-non-thinking', () => {
const shape = selectShape('claude-opus-4-7', { override: 'qwen-non-thinking' });
expect(shape.name).toBe('qwen-non-thinking');
});
it('override throws on unknown shape name', () => {
expect(() => selectShape('claude-opus-4-7', { override: 'nonexistent' })).toThrow(
/not in REGISTRY/,
);
});
});
describe('selector — registry inspection', () => {
it('listShapes returns all 5 expected names', () => {
const names = listShapes().sort();
expect(names).toEqual(['claude', 'generic-simple', 'gpt', 'qwen-non-thinking', 'qwen-thinking']);
});
it('REGISTRY contains all named shapes', () => {
expect(REGISTRY.claude).toBe(claudeShape);
expect(REGISTRY['qwen-thinking']).toBe(qwenThinkingShape);
expect(REGISTRY['qwen-non-thinking']).toBe(qwenNonThinkingShape);
expect(REGISTRY.gpt).toBe(gptShape);
expect(REGISTRY['generic-simple']).toBe(genericSimpleShape);
});
it('getShapeMetadata throws on unknown', () => {
expect(() => getShapeMetadata('nonexistent')).toThrow(/unknown shape/);
});
});
describe('selector — longest prefix wins', () => {
it('qwen3.6- specific prefix beats qwen- generic when both apply', () => {
// The config has both "qwen3.6-" and "qwen-" patterns, both mapping to
// qwen-thinking. Test that the longest-prefix rule is honored even when
// outcomes happen to match.
const shape = selectShape('qwen3.6-35b-a3b-something-new');
expect(shape.name).toBe('qwen-thinking');
});
});
describe('every shape — required metadata', () => {
for (const name of Object.keys(REGISTRY)) {
describe(`shape: ${name}`, () => {
const shape = REGISTRY[name];
it('has unique name matching registry key', () => {
expect(shape.name).toBe(name);
});
it('metadata.description is non-empty', () => {
expect(shape.metadata.description.length).toBeGreaterThan(0);
});
it('metadata.modelClass is non-empty', () => {
expect(shape.metadata.modelClass.length).toBeGreaterThan(0);
});
it('metadata.evidence_link is non-empty (HARD RULE)', () => {
expect(shape.metadata.evidence_link.length).toBeGreaterThan(0);
// No literal "TODO" or empty placeholders allowed.
expect(shape.metadata.evidence_link).not.toMatch(/^\s*todo\s*$/i);
});
});
}
});
describe('every shape — 4 build methods produce non-empty strings', () => {
for (const name of Object.keys(REGISTRY)) {
describe(`shape: ${name}`, () => {
const shape = REGISTRY[name];
it('systemPrompt (solo) is non-empty', () => {
const sp = shape.systemPrompt({ persona: 'You are X.', question: 'What is Y?', isMultiStep: false });
expect(sp.length).toBeGreaterThan(0);
expect(sp).toContain('X');
});
it('systemPrompt (multi-step) is non-empty and includes the action contract', () => {
const sp = shape.systemPrompt({
persona: 'You are X.',
question: 'What is Y?',
isMultiStep: true,
maxSteps: 5,
maxRetrievalsPerStep: 8,
});
expect(sp.length).toBeGreaterThan(0);
// Multi-step prompts must include the JSON action contract somehow.
expect(sp).toContain('"action"');
});
it('soloUserPrompt is non-empty and contains question', () => {
const up = shape.soloUserPrompt({
persona: 'You are X.',
materials: 'doc 1: foo\ndoc 2: bar',
question: 'What is Y?',
});
expect(up.length).toBeGreaterThan(0);
expect(up).toContain('What is Y?');
expect(up).toContain('foo');
});
it('multiStepKickoffUserPrompt is non-empty', () => {
const k = shape.multiStepKickoffUserPrompt({});
expect(k.length).toBeGreaterThan(0);
});
it('retrievalInjectionUserPrompt includes query and results', () => {
const ri = shape.retrievalInjectionUserPrompt({
query: 'find foo',
results: '[result 1] foo bar',
resultCount: 1,
});
expect(ri).toContain('find foo');
expect(ri).toContain('foo bar');
});
});
}
});
describe('shape distinguishes solo vs multi-step', () => {
it('claude shape: multi-step has protocol, solo does not', () => {
const solo = claudeShape.systemPrompt({ persona: 'P', question: 'Q', isMultiStep: false });
const multi = claudeShape.systemPrompt({ persona: 'P', question: 'Q', isMultiStep: true });
expect(solo).not.toContain('"action"');
expect(multi).toContain('"action"');
});
it('qwen-thinking shape: multi-step has protocol, solo does not', () => {
const solo = qwenThinkingShape.systemPrompt({ persona: 'P', question: 'Q', isMultiStep: false });
const multi = qwenThinkingShape.systemPrompt({ persona: 'P', question: 'Q', isMultiStep: true });
expect(solo).not.toContain('"action"');
expect(multi).toContain('"action"');
});
});
describe('MULTI_STEP_ACTION_CONTRACT', () => {
it('describes both retrieve and finalize actions', () => {
expect(MULTI_STEP_ACTION_CONTRACT).toContain('retrieve');
expect(MULTI_STEP_ACTION_CONTRACT).toContain('finalize');
});
it('is referenced by all 5 shapes in their multi-step prompts', () => {
// Indirect check: when the contract changes, all multi-step prompts pick up
// the change because each shape templates from MULTI_STEP_ACTION_CONTRACT.
for (const name of Object.keys(REGISTRY)) {
const shape = REGISTRY[name];
const sp = shape.systemPrompt({ persona: 'P', question: 'Q', isMultiStep: true });
// At minimum each shape must mention both action keywords.
expect(sp).toMatch(/retrieve/);
expect(sp).toMatch(/finalize/);
}
});
});
describe('config integration — model-prompt-shapes.json', () => {
it('cache flush + reload picks up config changes (no stale singleton)', () => {
// Smoke test of the cache reset path used by the test helper.
_resetConfigCache();
const shape1 = selectShape('claude-opus-4-7');
expect(shape1.name).toBe('claude');
});
});