/**
* Script Tools Integration Test
*
* Tests the complete script-tools workflow with real modules:
* - ScriptToolDefinitionLoader (real)
* - ToolDetectionService (real)
* - ExecutionPlanner (real)
*
* Mocks:
* - Filesystem reads (controlled fixtures)
* - Script execution (no actual Python/Node runtime)
*
* Classification: Integration (multiple real modules, mock I/O only)
*/
import { describe, expect, test, jest, beforeEach } from '@jest/globals';
import * as fs from 'fs';
import * as path from 'path';
import { ScriptToolDefinitionLoader } from '../../../src/modules/automation/core/script-definition-loader.js';
import { ToolDetectionService } from '../../../src/modules/automation/detection/tool-detection-service.js';
import { ExecutionPlanner } from '../../../src/engine/execution/planning/execution-planner.js';
import type { LoadedScriptTool } from '../../../src/modules/automation/types.js';
import type { ConvertedPrompt } from '../../../src/shared/types/index.js';
import type { Logger } from '../../../src/infra/logging/index.js';
import type { ContentAnalyzer } from '../../../src/modules/semantic/configurable-semantic-analyzer.js';
import type { ContentAnalysisResult } from '../../../src/modules/semantic/types.js';
// Mock filesystem for controlled fixtures
jest.mock('fs', () => ({
...jest.requireActual('fs'),
existsSync: jest.fn(),
readFileSync: jest.fn(),
readdirSync: jest.fn(),
statSync: jest.fn(),
}));
const mockFs = fs as jest.Mocked<typeof fs>;
const createLogger = (): Logger => ({
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
});
const createMockAnalyzer = (): Pick<ContentAnalyzer, 'analyzePrompt' | 'isLLMEnabled'> => {
const baseAnalysis: ContentAnalysisResult = {
executionType: 'single',
requiresExecution: true,
requiresFramework: false,
confidence: 0.85,
reasoning: [],
capabilities: {
canDetectStructure: true,
canAnalyzeComplexity: true,
canRecommendFramework: true,
hasSemanticUnderstanding: true,
},
limitations: [],
warnings: [],
executionCharacteristics: {
hasConditionals: false,
hasLoops: false,
hasChainSteps: false,
argumentCount: 1,
templateComplexity: 1,
hasSystemMessage: false,
hasUserTemplate: true,
hasStructuredReasoning: false,
hasMethodologyKeywords: false,
hasComplexAnalysis: false,
},
complexity: 'medium',
suggestedGates: [],
frameworkRecommendation: {
shouldUseFramework: false,
reasoning: [],
confidence: 0.4,
},
analysisMetadata: {
version: 'test',
mode: 'minimal',
analysisTime: 5,
analyzer: 'content',
cacheHit: false,
},
};
return {
analyzePrompt: jest.fn().mockResolvedValue(baseAnalysis),
isLLMEnabled: jest.fn().mockReturnValue(true),
};
};
describe('Script Tools Workflow Integration', () => {
let logger: Logger;
let analyzer: Pick<ContentAnalyzer, 'analyzePrompt' | 'isLLMEnabled'>;
beforeEach(() => {
jest.clearAllMocks();
logger = createLogger();
analyzer = createMockAnalyzer();
});
describe('Tool Detection → Execution Planning Flow', () => {
test('detects tools from user args and plans clean mode execution', async () => {
// Arrange: Real services working together
const detectionService = new ToolDetectionService();
const planner = new ExecutionPlanner(analyzer, logger);
// Fixture: Loaded script tool (simulating what ScriptToolDefinitionLoader produces)
const loadedTool: LoadedScriptTool = {
id: 'word_count',
name: 'Word Counter',
description: 'Counts words in text',
scriptPath: 'script.py',
runtime: 'python',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', description: 'Text to analyze' },
},
required: ['text'],
},
toolDir: '/prompts/test_prompt/tools/word_count',
absoluteScriptPath: '/prompts/test_prompt/tools/word_count/script.py',
promptId: 'test_prompt',
descriptionContent: 'Counts words, characters, and lines in text.',
enabled: true,
};
// Fixture: Prompt with script tools
const promptWithTools: ConvertedPrompt = {
id: 'test_prompt',
name: 'Test Prompt',
description: 'Test prompt with script tools',
category: 'general',
userMessageTemplate: 'Analyze: {{text}}',
arguments: [
{ name: 'text', description: 'Text to analyze', required: true, type: 'string' },
],
scriptTools: [loadedTool],
};
// Act: Detection phase (real ToolDetectionService)
const userArgs = { text: 'Hello world' };
const matches = detectionService.detectTools('', userArgs, [loadedTool]);
// Act: Planning phase (real ExecutionPlanner)
const plan = await planner.createPlan({
convertedPrompt: promptWithTools,
frameworkEnabled: true,
});
// Assert: Tool was detected via parameter/schema match
expect(matches.length).toBe(1);
expect(matches[0].toolId).toBe('word_count');
expect(matches[0].priority).toBeGreaterThanOrEqual(0.8);
expect(matches[0].matchReason).toBe('schema_match');
// Assert: Planner applied clean mode default
expect(plan.modifiers?.clean).toBe(true);
expect(plan.requiresFramework).toBe(false);
});
test('respects explicit modifier override in integrated flow', async () => {
// Arrange
const planner = new ExecutionPlanner(analyzer, logger);
const loadedTool: LoadedScriptTool = {
id: 'analyzer',
name: 'Analyzer',
description: 'Analyzes content',
scriptPath: 'script.py',
runtime: 'python',
inputSchema: { type: 'object', properties: {}, required: [] },
toolDir: '/tools/analyzer',
absoluteScriptPath: '/tools/analyzer/script.py',
promptId: 'test',
enabled: true,
};
const promptWithTools: ConvertedPrompt = {
id: 'test',
name: 'Test',
description: 'Test',
category: 'general',
userMessageTemplate: 'Test',
arguments: [],
scriptTools: [loadedTool],
};
// Act: User explicitly requests %framework mode
const plan = await planner.createPlan({
convertedPrompt: promptWithTools,
frameworkEnabled: true,
parsedCommand: {
promptId: 'test',
rawArgs: '',
format: 'symbolic',
confidence: 0.9,
modifiers: { framework: true }, // Explicit override
metadata: {
originalCommand: '%framework >>test',
parseStrategy: 'symbolic',
detectedFormat: 'symbolic',
warnings: [],
},
},
});
// Assert: User's explicit modifier takes precedence
expect(plan.modifiers?.framework).toBe(true);
expect(plan.requiresFramework).toBe(true);
});
test('custom gates override clean default in integrated flow', async () => {
// Arrange
const planner = new ExecutionPlanner(analyzer, logger);
const loadedTool: LoadedScriptTool = {
id: 'validator',
name: 'Validator',
description: 'Validates input',
scriptPath: 'script.py',
runtime: 'python',
inputSchema: { type: 'object', properties: {}, required: [] },
toolDir: '/tools/validator',
absoluteScriptPath: '/tools/validator/script.py',
promptId: 'test',
enabled: true,
};
const promptWithTools: ConvertedPrompt = {
id: 'test',
name: 'Test',
description: 'Test',
category: 'general',
userMessageTemplate: 'Test',
arguments: [],
scriptTools: [loadedTool],
};
// Act: User provides custom gates
const plan = await planner.createPlan({
convertedPrompt: promptWithTools,
frameworkEnabled: true,
gateOverrides: {
gates: ['code-quality', 'security-review'],
},
});
// Assert: Clean default NOT applied (gates signal user intent)
expect(plan.modifiers?.clean).toBeFalsy();
// Gates should be present
expect(plan.gates).toContain('code-quality');
expect(plan.gates).toContain('security-review');
});
});
describe('Input Extraction', () => {
test('extracts inputs matching schema properties', () => {
const service = new ToolDetectionService();
const tool: LoadedScriptTool = {
id: 'formatter',
name: 'Formatter',
description: 'Formats content',
scriptPath: 'script.py',
runtime: 'python',
inputSchema: {
type: 'object',
properties: {
input_text: { type: 'string' },
output_format: { type: 'string' },
},
required: ['input_text'],
},
toolDir: '/tools/formatter',
absoluteScriptPath: '/tools/formatter/script.py',
promptId: 'test',
enabled: true,
};
const args = {
input_text: 'Hello World',
output_format: 'markdown',
extra_field: 'ignored',
};
const inputs = service.extractInputs(args, tool);
expect(inputs).toEqual({
input_text: 'Hello World',
output_format: 'markdown',
});
expect(inputs).not.toHaveProperty('extra_field');
});
test('handles camelCase to snake_case conversion', () => {
const service = new ToolDetectionService();
const tool: LoadedScriptTool = {
id: 'converter',
name: 'Converter',
description: 'Converts data',
scriptPath: 'script.py',
runtime: 'python',
inputSchema: {
type: 'object',
properties: {
file_path: { type: 'string' },
},
required: ['file_path'],
},
toolDir: '/tools/converter',
absoluteScriptPath: '/tools/converter/script.py',
promptId: 'test',
enabled: true,
};
// User provides camelCase, schema expects snake_case
const args = { filePath: '/path/to/file.txt' };
const inputs = service.extractInputs(args, tool);
expect(inputs.file_path).toBe('/path/to/file.txt');
});
});
});