tractatus/tests/unit/services/VariableSubstitution.service.test.js
TheFlow c96ad31046 feat: implement Rule Manager and Project Manager admin systems
Major Features:
- Multi-project governance with Rule Manager web UI
- Project Manager for organizing governance across projects
- Variable substitution system (${VAR_NAME} in rules)
- Claude.md analyzer for instruction extraction
- Rule quality scoring and optimization

Admin UI Components:
- /admin/rule-manager.html - Full-featured rule management interface
- /admin/project-manager.html - Multi-project administration
- /admin/claude-md-migrator.html - Import rules from Claude.md files
- Dashboard enhancements for governance analytics

Backend Implementation:
- Controllers: projects, rules, variables
- Models: Project, VariableValue, enhanced GovernanceRule
- Routes: /api/projects, /api/rules with full CRUD
- Services: ClaudeMdAnalyzer, RuleOptimizer, VariableSubstitution
- Utilities: mongoose helpers

Documentation:
- User guides for Rule Manager and Projects
- Complete API documentation (PROJECTS_API, RULES_API)
- Phase 3 planning and architecture diagrams
- Test results and error analysis
- Coding best practices summary

Testing & Scripts:
- Integration tests for projects API
- Unit tests for variable substitution
- Database migration scripts
- Seed data generation
- Test token generator

Key Capabilities:
 UNIVERSAL scope rules apply across all projects
 PROJECT_SPECIFIC rules override for individual projects
 Variable substitution per-project (e.g., ${DB_PORT} → 27017)
 Real-time validation and quality scoring
 Advanced filtering and search
 Import from existing Claude.md files

Technical Details:
- MongoDB-backed governance persistence
- RESTful API with Express
- JWT authentication for admin endpoints
- CSP-compliant frontend (no inline handlers)
- Responsive Tailwind UI

This implements Phase 3 architecture as documented in planning docs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-11 17:16:51 +13:00

254 lines
9 KiB
JavaScript

/**
* Variable Substitution Service - Unit Tests
*
* Tests the core variable substitution logic without database dependencies.
* Integration tests will cover database interactions.
*/
const VariableSubstitutionService = require('../../../src/services/VariableSubstitution.service');
describe('VariableSubstitutionService', () => {
describe('extractVariables', () => {
it('should extract single variable from text', () => {
const text = 'Use database ${DB_NAME}';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['DB_NAME']);
});
it('should extract multiple variables from text', () => {
const text = 'Use ${DB_NAME} on port ${DB_PORT} in ${ENVIRONMENT}';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['DB_NAME', 'DB_PORT', 'ENVIRONMENT']);
});
it('should remove duplicate variables', () => {
const text = 'Copy ${FILE_PATH} to ${FILE_PATH} backup';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['FILE_PATH']);
});
it('should handle text with no variables', () => {
const text = 'No variables in this text';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual([]);
});
it('should handle empty string', () => {
const result = VariableSubstitutionService.extractVariables('');
expect(result).toEqual([]);
});
it('should handle null or undefined input', () => {
expect(VariableSubstitutionService.extractVariables(null)).toEqual([]);
expect(VariableSubstitutionService.extractVariables(undefined)).toEqual([]);
});
it('should only match UPPER_SNAKE_CASE variables', () => {
const text = 'Valid: ${DB_NAME} ${API_KEY_2} Invalid: ${lowercase} ${Mixed_Case}';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['DB_NAME', 'API_KEY_2']);
});
it('should match variables with numbers', () => {
const text = 'Use ${DB_PORT_3306} and ${API_V2_KEY}';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['DB_PORT_3306', 'API_V2_KEY']);
});
it('should ignore incomplete placeholders', () => {
const text = 'Incomplete: ${ DB_NAME} ${DB_NAME } $DB_NAME';
const result = VariableSubstitutionService.extractVariables(text);
// Should find none because they don't match the strict pattern
expect(result).toEqual([]);
});
it('should handle variables at start and end of text', () => {
const text = '${START_VAR} middle text ${END_VAR}';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['START_VAR', 'END_VAR']);
});
it('should handle multiline text', () => {
const text = `
Line 1 has \${VAR_1}
Line 2 has \${VAR_2}
Line 3 has \${VAR_3}
`;
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['VAR_1', 'VAR_2', 'VAR_3']);
});
});
describe('getSuggestedVariables', () => {
it('should return variable metadata with positions', () => {
const text = 'Use ${DB_NAME} and ${DB_PORT}';
const result = VariableSubstitutionService.getSuggestedVariables(text);
expect(result).toHaveLength(2);
expect(result[0]).toMatchObject({
name: 'DB_NAME',
placeholder: '${DB_NAME}',
positions: expect.arrayContaining([expect.any(Number)])
});
});
it('should track multiple occurrences of same variable', () => {
const text = '${VAR} appears ${VAR} twice ${VAR}';
const result = VariableSubstitutionService.getSuggestedVariables(text);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('VAR');
expect(result[0].positions).toHaveLength(3);
});
it('should handle text with no variables', () => {
const text = 'No variables here';
const result = VariableSubstitutionService.getSuggestedVariables(text);
expect(result).toEqual([]);
});
it('should handle empty or invalid input', () => {
expect(VariableSubstitutionService.getSuggestedVariables(null)).toEqual([]);
expect(VariableSubstitutionService.getSuggestedVariables(undefined)).toEqual([]);
expect(VariableSubstitutionService.getSuggestedVariables('')).toEqual([]);
});
});
describe('Edge Cases and Error Handling', () => {
it('should handle text with special characters around variables', () => {
const text = 'Path: /${BASE_PATH}/${SUB_PATH}/file.txt';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['BASE_PATH', 'SUB_PATH']);
});
it('should handle text with escaped characters', () => {
const text = 'Use \\${NOT_A_VAR} and ${REAL_VAR}';
const result = VariableSubstitutionService.extractVariables(text);
// The service doesn't handle escaping, so both would be matched
// This is expected behavior - escaping handled at different layer
expect(result).toContain('REAL_VAR');
});
it('should handle very long variable names', () => {
const longName = 'A'.repeat(100);
const text = `Use \${${longName}}`;
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual([longName]);
});
it('should handle text with nested-looking braces', () => {
const text = 'Not nested: ${VAR_1} { ${VAR_2} }';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['VAR_1', 'VAR_2']);
});
it('should handle Unicode text around variables', () => {
const text = 'Unicode: 你好 ${VAR_NAME} 世界';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['VAR_NAME']);
});
it('should handle variables in JSON-like strings', () => {
const text = '{"key": "${VALUE}", "port": ${PORT_NUMBER}}';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['VALUE', 'PORT_NUMBER']);
});
it('should handle variables in SQL-like strings', () => {
const text = 'SELECT * FROM ${TABLE_NAME} WHERE id = ${USER_ID}';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['TABLE_NAME', 'USER_ID']);
});
it('should handle variables in shell-like strings', () => {
const text = 'export PATH="${BIN_PATH}:$PATH"';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['BIN_PATH']);
});
});
describe('Variable Name Validation', () => {
it('should reject variables starting with numbers', () => {
const text = '${123VAR} ${VAR123}';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['VAR123']); // Only VAR123 is valid
});
it('should reject variables with special characters', () => {
const text = '${VAR-NAME} ${VAR.NAME} ${VAR@NAME} ${VAR_NAME}';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['VAR_NAME']); // Only underscores allowed
});
it('should reject lowercase variables', () => {
const text = '${lowercase} ${UPPERCASE} ${MixedCase}';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['UPPERCASE']);
});
it('should accept single letter variables', () => {
const text = '${A} ${B} ${C}';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['A', 'B', 'C']);
});
it('should handle consecutive underscores', () => {
const text = '${VAR__NAME} ${VAR___NAME}';
const result = VariableSubstitutionService.extractVariables(text);
expect(result).toEqual(['VAR__NAME', 'VAR___NAME']);
});
});
describe('Performance and Scalability', () => {
it('should handle text with many variables efficiently', () => {
const variableCount = 100;
const variables = Array.from({ length: variableCount }, (_, i) => `VAR_${i}`);
const text = variables.map(v => `\${${v}}`).join(' ');
const startTime = Date.now();
const result = VariableSubstitutionService.extractVariables(text);
const endTime = Date.now();
expect(result).toHaveLength(variableCount);
expect(endTime - startTime).toBeLessThan(100); // Should complete in < 100ms
});
it('should handle very long text efficiently', () => {
const longText = 'Some text '.repeat(10000) + '${VAR_1} and ${VAR_2}';
const startTime = Date.now();
const result = VariableSubstitutionService.extractVariables(longText);
const endTime = Date.now();
expect(result).toEqual(['VAR_1', 'VAR_2']);
expect(endTime - startTime).toBeLessThan(100);
});
});
});
// Note: Integration tests for substituteVariables, substituteRule, etc.
// will be in tests/integration/ as they require database mocking/setup