Restructure chat service, web routes, and add test suite
Split monolithic chat service into modular structure with Matrix client support, extract web server into per-resource route modules, introduce bootstrap/main-loop separation, and add comprehensive vitest coverage across services, repositories, and utilities. Adds project docs and CLAUDE.md agent instructions.
This commit is contained in:
1 parent
dfa0580859
commit
633bb0ec04
84 files changed
+19278
-3888
No files matched your search
@@ -0,0 +1,223 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { ConfigSchema } from '../src/config/index.js';
|
||||
|
||||
describe('Prompt Testing', () => {
|
||||
const configPath = path.resolve(process.cwd(), 'config.yaml');
|
||||
|
||||
describe('config.yaml prompts', () => {
|
||||
it('loads successfully', () => {
|
||||
expect(fs.existsSync(configPath)).toBe(true);
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
expect(content).toContain('imageAnalysisPrompt');
|
||||
expect(content).toContain('messageGenerationPrompt');
|
||||
});
|
||||
|
||||
it('imageAnalysisPrompt requests body features', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const prompts = parseYamlPrompts(content);
|
||||
|
||||
expect(prompts.imageAnalysisPrompt).toContain('breasts');
|
||||
expect(prompts.imageAnalysisPrompt).toContain('ASS');
|
||||
expect(prompts.imageAnalysisPrompt).toContain('vagina');
|
||||
expect(prompts.imageAnalysisPrompt).toContain('BODY');
|
||||
expect(prompts.imageAnalysisPrompt).toContain('POSE');
|
||||
expect(prompts.imageAnalysisPrompt).toContain('VIBE');
|
||||
expect(prompts.imageAnalysisPrompt).toContain('EXTRAS');
|
||||
});
|
||||
|
||||
it('messageGenerationPrompt has proper structure', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const prompts = parseYamlPrompts(content);
|
||||
|
||||
expect(prompts.messageGenerationPrompt).toContain('{analysis}');
|
||||
expect(prompts.messageGenerationPrompt).toContain('{title}');
|
||||
expect(prompts.messageGenerationPrompt).toContain('{author}');
|
||||
});
|
||||
|
||||
it('messageGenerationPrompt specifies DO NOT list', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const prompts = parseYamlPrompts(content);
|
||||
|
||||
expect(prompts.messageGenerationPrompt).toContain('DO NOT');
|
||||
expect(prompts.messageGenerationPrompt).toContain('captivating');
|
||||
expect(prompts.messageGenerationPrompt).toContain('alluring');
|
||||
expect(prompts.messageGenerationPrompt).toContain('exquisite');
|
||||
expect(prompts.messageGenerationPrompt).toContain('radiant');
|
||||
});
|
||||
|
||||
it('messageGenerationPrompt specifies GOOD HOOKS', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const prompts = parseYamlPrompts(content);
|
||||
|
||||
expect(prompts.messageGenerationPrompt).toContain('GOOD HOOKS');
|
||||
expect(prompts.messageGenerationPrompt).toContain('BAD HOOKS');
|
||||
});
|
||||
|
||||
it('messageGenerationPrompt specifies language requirements', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const prompts = parseYamlPrompts(content);
|
||||
|
||||
expect(prompts.messageGenerationPrompt).toContain('explicit terms');
|
||||
expect(prompts.messageGenerationPrompt).toContain('tits');
|
||||
expect(prompts.messageGenerationPrompt).toContain('ass');
|
||||
expect(prompts.messageGenerationPrompt).toContain('pussy');
|
||||
expect(prompts.messageGenerationPrompt).toContain('boobs');
|
||||
});
|
||||
|
||||
it('messageGenerationPrompt specifies what to avoid', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const prompts = parseYamlPrompts(content);
|
||||
|
||||
expect(prompts.messageGenerationPrompt).toContain('breasts');
|
||||
expect(prompts.messageGenerationPrompt).toContain('buttocks');
|
||||
expect(prompts.messageGenerationPrompt).toContain('posterior');
|
||||
expect(prompts.messageGenerationPrompt).toContain('physique');
|
||||
});
|
||||
});
|
||||
|
||||
describe('strategy prompts', () => {
|
||||
it('loads strategy variants', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const yaml = require('yaml');
|
||||
const config = yaml.parse(content);
|
||||
|
||||
expect(config.strategies.variants).toBeDefined();
|
||||
expect(config.strategies.variants.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('each strategy has id and promptOverride', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const yaml = require('yaml');
|
||||
const config = yaml.parse(content);
|
||||
|
||||
for (const variant of config.strategies.variants) {
|
||||
expect(variant.id).toBeDefined();
|
||||
expect(variant.promptOverride).toBeDefined();
|
||||
expect(typeof variant.id).toBe('string');
|
||||
expect(typeof variant.promptOverride).toBe('string');
|
||||
expect(variant.id.length).toBeGreaterThan(0);
|
||||
expect(variant.promptOverride.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('strategy prompts are distinct', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const yaml = require('yaml');
|
||||
const config = yaml.parse(content);
|
||||
|
||||
const prompts = config.strategies.variants.map(v => v.promptOverride.toLowerCase());
|
||||
const uniquePrompts = new Set(prompts);
|
||||
expect(uniquePrompts.size).toBe(prompts.length);
|
||||
});
|
||||
|
||||
it('strategy variants have valid IDs', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const yaml = require('yaml');
|
||||
const config = yaml.parse(content);
|
||||
|
||||
for (const variant of config.strategies.variants) {
|
||||
expect(variant.id).toMatch(/^[a-z0-9_]+$/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('prompt variable substitution', () => {
|
||||
it('imageAnalysisPrompt contains all required variables', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const yaml = require('yaml');
|
||||
const config = yaml.parse(content);
|
||||
|
||||
// The image analysis prompt should not contain unsubstitutable variables
|
||||
// (it receives raw image data, not text variables)
|
||||
expect(config.openrouter.imageAnalysisPrompt).toBeDefined();
|
||||
});
|
||||
|
||||
it('messageGenerationPrompt contains all substitution variables', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const yaml = require('yaml');
|
||||
const config = yaml.parse(content);
|
||||
|
||||
expect(config.openrouter.messageGenerationPrompt).toContain('{analysis}');
|
||||
expect(config.openrouter.messageGenerationPrompt).toContain('{title}');
|
||||
expect(config.openrouter.messageGenerationPrompt).toContain('{author}');
|
||||
});
|
||||
|
||||
it('all substitution variables are in {variable} format', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const yaml = require('yaml');
|
||||
const config = yaml.parse(content);
|
||||
|
||||
const variables = config.openrouter.messageGenerationPrompt.match(/\{(\w+)\}/g);
|
||||
expect(variables).toEqual(expect.arrayContaining(['{analysis}', '{title}', '{author}']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('prompt quality checks', () => {
|
||||
it('messageGenerationPrompt is not empty', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const yaml = require('yaml');
|
||||
const config = yaml.parse(content);
|
||||
|
||||
expect(config.openrouter.messageGenerationPrompt.trim().length).toBeGreaterThan(50);
|
||||
});
|
||||
|
||||
it('imageAnalysisPrompt is not empty', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const yaml = require('yaml');
|
||||
const config = yaml.parse(content);
|
||||
|
||||
expect(config.openrouter.imageAnalysisPrompt.trim().length).toBeGreaterThan(50);
|
||||
});
|
||||
|
||||
it('messageGenerationPrompt has examples', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const yaml = require('yaml');
|
||||
const config = yaml.parse(content);
|
||||
|
||||
expect(config.openrouter.messageGenerationPrompt).toContain('Examples of good messages');
|
||||
expect(config.openrouter.messageGenerationPrompt).toContain('"Damn those tits');
|
||||
});
|
||||
|
||||
it('imageAnalysisPrompt requests a rating', () => {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const yaml = require('yaml');
|
||||
const config = yaml.parse(content);
|
||||
|
||||
expect(config.openrouter.imageAnalysisPrompt).toContain('rate her attractiveness');
|
||||
expect(config.openrouter.imageAnalysisPrompt).toContain('1-100 scale');
|
||||
expect(config.openrouter.imageAnalysisPrompt).toContain('RATING:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('seller keywords', () => {
|
||||
function loadParsedConfig() {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const yaml = require('yaml');
|
||||
const rawConfig = yaml.parse(content);
|
||||
rawConfig.openrouter = rawConfig.openrouter || {};
|
||||
rawConfig.openrouter.apiKey = rawConfig.openrouter.apiKey || 'test';
|
||||
return ConfigSchema.parse(rawConfig);
|
||||
}
|
||||
|
||||
it('reddit sellerKeywords includes common platforms', () => {
|
||||
const config = loadParsedConfig();
|
||||
const keywords = config.reddit.sellerKeywords.map((k: string) => k.toLowerCase());
|
||||
expect(keywords).toContain('onlyfans');
|
||||
expect(keywords).toContain('fansly');
|
||||
expect(keywords).toContain('manyvids');
|
||||
expect(keywords).toContain('cash.app');
|
||||
expect(keywords).toContain('venmo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function parseYamlPrompts(content: string): { imageAnalysisPrompt: string; messageGenerationPrompt: string } {
|
||||
const yaml = require('yaml');
|
||||
const config = yaml.parse(content);
|
||||
return {
|
||||
imageAnalysisPrompt: config.openrouter.imageAnalysisPrompt,
|
||||
messageGenerationPrompt: config.openrouter.messageGenerationPrompt,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user