/**
* Particles Tools Tests
* ISO/IEC 29119 compliant - comprehensive test coverage
* ISO/IEC 25010 compliant - strict testing
*
* Tests for:
* - CreateGPUParticlesTool
* - CreateParticleMaterialTool
*/
import { handleCreateGPUParticles } from './CreateGPUParticlesTool';
import { handleCreateParticleMaterial } from './CreateParticleMaterialTool';
import { createTempProject } from '../test-utils.js';
describe('Particles Tools', () => {
// ============================================================================
// CreateGPUParticles Tests
// ============================================================================
describe('CreateGPUParticles', () => {
describe('ISO 29119-4: Input Validation', () => {
describe('Required Parameter Validation', () => {
it('should return error when projectPath is missing', async () => {
const result = await handleCreateGPUParticles({
scenePath: 'scenes/main.tscn',
nodeName: 'Particles',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('projectPath');
});
it('should return error when scenePath is missing', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/path/to/project',
nodeName: 'Particles',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('scenePath');
});
it('should return error when nodeName is missing', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/path/to/project',
scenePath: 'scenes/main.tscn',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('nodeName');
});
it('should return error for empty projectPath', async () => {
const result = await handleCreateGPUParticles({
projectPath: '',
scenePath: 'scenes/main.tscn',
nodeName: 'Particles',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/projectPath|empty/i);
});
it('should return error for empty scenePath', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/path/to/project',
scenePath: '',
nodeName: 'Particles',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/scenePath|empty/i);
});
it('should return error for empty nodeName', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/path/to/project',
scenePath: 'scenes/main.tscn',
nodeName: '',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/nodeName|empty/i);
});
});
describe('Optional Parameter Validation', () => {
it('should accept valid amount', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Particles',
amount: 100,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept valid lifetime', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Particles',
lifetime: 2.0,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept oneShot flag', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Particles',
oneShot: true,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept preprocess value', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Particles',
preprocess: 0.5,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept emitting flag', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Particles',
emitting: false,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept materialPath', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Particles',
materialPath: 'particles/fire_mat.tres',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept parentNodePath', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Particles',
parentNodePath: 'Effects',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept all optional parameters together', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'FullParticles',
parentNodePath: 'Effects',
is3D: true,
amount: 200,
lifetime: 3.0,
oneShot: false,
preprocess: 0.1,
emitting: true,
materialPath: 'particles/smoke.tres',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
});
describe('Dimension Flag Validation', () => {
it('should accept is3D: true', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Particles3D',
is3D: true,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept is3D: false', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Particles2D',
is3D: false,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should default to is3D: false when not specified', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Particles',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
});
describe('Path Security Validation', () => {
it('should return error for path traversal in projectPath', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/path/../../../etc/passwd',
scenePath: 'scenes/main.tscn',
nodeName: 'Particles',
});
expect(result.isError).toBe(true);
// On Windows, path resolution may resolve '..' before validation
// Accept either path traversal error or invalid project error
expect(result.content[0].text).toMatch(/path traversal|\.\.|Not a valid Godot project/i);
});
it('should return error for path traversal in scenePath', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/path/to/project',
scenePath: '../../../etc/passwd.tscn',
nodeName: 'Particles',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/path traversal|\.\./i);
});
});
describe('Project Validation', () => {
it('should return error for non-existent project path', async () => {
const result = await handleCreateGPUParticles({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Particles',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
});
describe('Scene Validation', () => {
let projectPath: string;
let cleanup: () => void;
beforeEach(() => {
const temp = createTempProject();
projectPath = temp.projectPath;
cleanup = temp.cleanup;
});
afterEach(() => {
cleanup();
});
it('should return error for non-existent scene', async () => {
const result = await handleCreateGPUParticles({
projectPath,
scenePath: 'scenes/nonexistent.tscn',
nodeName: 'Particles',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/scene|not found|does not exist/i);
});
});
});
describe('ISO 29119-4: Success Scenarios', () => {
let projectPath: string;
let cleanup: () => void;
beforeEach(() => {
const temp = createTempProject();
projectPath = temp.projectPath;
cleanup = temp.cleanup;
});
afterEach(() => {
cleanup();
});
it('should pass validation for 2D GPU particles request', async () => {
const result = await handleCreateGPUParticles({
projectPath,
scenePath: 'scenes/main.tscn',
nodeName: 'FireParticles',
is3D: false,
});
// Validation should pass (may fail on Godot execution)
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for 3D GPU particles request', async () => {
const result = await handleCreateGPUParticles({
projectPath,
scenePath: 'scenes/main.tscn',
nodeName: 'SmokeParticles',
is3D: true,
});
expect(result.content[0].text).toBeDefined();
});
it('should pass validation with particle configuration', async () => {
const result = await handleCreateGPUParticles({
projectPath,
scenePath: 'scenes/main.tscn',
nodeName: 'ExplosionParticles',
is3D: false,
amount: 500,
lifetime: 0.5,
oneShot: true,
preprocess: 0.2,
});
expect(result.content[0].text).toBeDefined();
});
it('should pass validation with parent node specified', async () => {
const result = await handleCreateGPUParticles({
projectPath,
scenePath: 'scenes/main.tscn',
nodeName: 'PlayerParticles',
parentNodePath: 'Player',
});
expect(result.content[0].text).toBeDefined();
});
it('should pass validation with material path specified', async () => {
const result = await handleCreateGPUParticles({
projectPath,
scenePath: 'scenes/main.tscn',
nodeName: 'StyledParticles',
materialPath: 'particles/custom.tres',
});
expect(result.content[0].text).toBeDefined();
});
});
});
// ============================================================================
// CreateParticleMaterial Tests
// ============================================================================
describe('CreateParticleMaterial', () => {
describe('ISO 29119-4: Input Validation', () => {
describe('Required Parameter Validation', () => {
it('should return error when projectPath is missing', async () => {
const result = await handleCreateParticleMaterial({
materialPath: 'particles/fire_mat.tres',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('projectPath');
});
it('should return error when materialPath is missing', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/path/to/project',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('materialPath');
});
it('should return error for empty projectPath', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '',
materialPath: 'particles/fire_mat.tres',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/projectPath|empty/i);
});
it('should return error for empty materialPath', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/path/to/project',
materialPath: '',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/materialPath|empty/i);
});
});
describe('File Extension Validation', () => {
it('should return error for invalid material extension (.mat)', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/fire_mat.mat',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/\.tres|\.res/i);
});
it('should return error for invalid material extension (.txt)', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/fire_mat.txt',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/\.tres|\.res/i);
});
it('should return error for invalid material extension (.gd)', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/fire_mat.gd',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/\.tres|\.res/i);
});
it('should return error for no file extension', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/fire_mat',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/\.tres|\.res/i);
});
it('should accept .tres extension', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/fire_mat.tres',
});
expect(result.isError).toBe(true);
// Should fail on project validation, not extension validation
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept .res extension', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/fire_mat.res',
});
expect(result.isError).toBe(true);
// Should fail on project validation, not extension validation
expect(result.content[0].text).toContain('Not a valid Godot project');
});
});
describe('Emission Shape Enum Validation', () => {
it('should accept point emission shape', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/point.tres',
emissionShape: 'point',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept sphere emission shape', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/sphere.tres',
emissionShape: 'sphere',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept sphere_surface emission shape', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/sphere_surface.tres',
emissionShape: 'sphere_surface',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept box emission shape', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/box.tres',
emissionShape: 'box',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept ring emission shape', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/ring.tres',
emissionShape: 'ring',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should return error for invalid emission shape', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/path/to/project',
materialPath: 'particles/invalid.tres',
emissionShape: 'invalid' as 'point',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/emissionShape|Invalid input/i);
});
});
describe('Direction Vector Validation', () => {
it('should accept valid direction vector', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/directional.tres',
direction: { x: 0, y: -1, z: 0 },
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept custom direction vector', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/custom_dir.tres',
direction: { x: 1, y: 1, z: 1 },
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept negative direction values', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/neg_dir.tres',
direction: { x: -0.5, y: -1, z: 0.5 },
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
});
describe('Gravity Vector Validation', () => {
it('should accept valid gravity vector', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/gravity.tres',
gravity: { x: 0, y: 98, z: 0 },
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept zero gravity', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/no_gravity.tres',
gravity: { x: 0, y: 0, z: 0 },
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept negative gravity (upward force)', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/up_gravity.tres',
gravity: { x: 0, y: -50, z: 0 },
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
});
describe('Velocity Parameters Validation', () => {
it('should accept initialVelocityMin', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/velocity_min.tres',
initialVelocityMin: 10,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept initialVelocityMax', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/velocity_max.tres',
initialVelocityMax: 50,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept velocity range', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/velocity_range.tres',
initialVelocityMin: 10,
initialVelocityMax: 50,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
});
describe('Angular Velocity Parameters Validation', () => {
it('should accept angularVelocityMin', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/ang_vel_min.tres',
angularVelocityMin: -180,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept angularVelocityMax', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/ang_vel_max.tres',
angularVelocityMax: 180,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept angular velocity range', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/ang_vel_range.tres',
angularVelocityMin: -90,
angularVelocityMax: 90,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
});
describe('Scale Parameters Validation', () => {
it('should accept scaleMin', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/scale_min.tres',
scaleMin: 0.5,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept scaleMax', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/scale_max.tres',
scaleMax: 2.0,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept scale range', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/scale_range.tres',
scaleMin: 0.5,
scaleMax: 2.0,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
});
describe('Color Validation', () => {
it('should accept valid color', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/colored.tres',
color: { r: 1, g: 0.5, b: 0 },
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept color with alpha', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/colored_alpha.tres',
color: { r: 1, g: 0.5, b: 0, a: 0.8 },
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept white color', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/white.tres',
color: { r: 1, g: 1, b: 1 },
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept black color', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/black.tres',
color: { r: 0, g: 0, b: 0 },
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
});
describe('Spread Angle Validation', () => {
it('should accept valid spread angle', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/spread.tres',
spread: 45,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept zero spread', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/no_spread.tres',
spread: 0,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept maximum spread (180)', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/full_spread.tres',
spread: 180,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
});
describe('Path Security Validation', () => {
it('should return error for path traversal in projectPath', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/path/../../../etc/passwd',
materialPath: 'particles/fire_mat.tres',
});
expect(result.isError).toBe(true);
// On Windows, path resolution may resolve '..' before validation
// Accept either path traversal error or invalid project error
expect(result.content[0].text).toMatch(/path traversal|\.\.|Not a valid Godot project/i);
});
it('should return error for path traversal in materialPath', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/path/to/project',
materialPath: '../../../etc/passwd.tres',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/path traversal|\.\./i);
});
});
describe('Project Validation', () => {
it('should return error for non-existent project path', async () => {
const result = await handleCreateParticleMaterial({
projectPath: '/non/existent/path',
materialPath: 'particles/fire_mat.tres',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
});
});
describe('ISO 29119-4: Success Scenarios', () => {
let projectPath: string;
let cleanup: () => void;
beforeEach(() => {
const temp = createTempProject();
projectPath = temp.projectPath;
cleanup = temp.cleanup;
});
afterEach(() => {
cleanup();
});
it('should pass validation for particle material with default values', async () => {
const result = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/default_mat.tres',
});
// Validation should pass
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for particle material with emission shape', async () => {
const result = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/sphere_emit.tres',
emissionShape: 'sphere',
});
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for particle material with box emission', async () => {
const result = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/box_emit.tres',
emissionShape: 'box',
});
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for particle material with custom direction', async () => {
const result = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/directional.tres',
direction: { x: 1, y: 0, z: 0 },
});
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for particle material with custom gravity', async () => {
const result = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/custom_gravity.tres',
gravity: { x: 0, y: 50, z: 0 },
});
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for particle material with velocity settings', async () => {
const result = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/velocity.tres',
initialVelocityMin: 10,
initialVelocityMax: 50,
});
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for particle material with angular velocity', async () => {
const result = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/angular.tres',
angularVelocityMin: -180,
angularVelocityMax: 180,
});
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for particle material with scale settings', async () => {
const result = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/scaled.tres',
scaleMin: 0.5,
scaleMax: 2.0,
});
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for particle material with color', async () => {
const result = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/colored.tres',
color: { r: 1, g: 0.5, b: 0, a: 0.8 },
});
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for fire-like particle material with all settings', async () => {
const result = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/fire.tres',
emissionShape: 'box',
direction: { x: 0, y: -1, z: 0 },
spread: 15,
gravity: { x: 0, y: -20, z: 0 },
initialVelocityMin: 20,
initialVelocityMax: 40,
scaleMin: 0.5,
scaleMax: 1.5,
color: { r: 1, g: 0.5, b: 0, a: 1 },
});
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for material in nested directory', async () => {
const result = await handleCreateParticleMaterial({
projectPath,
materialPath: 'resources/effects/particles/smoke.tres',
});
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for material with .res extension', async () => {
const result = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/binary_mat.res',
});
expect(result.content[0].text).toBeDefined();
});
});
});
// ============================================================================
// Integration Tests - Validation Only
// Note: Full file creation tests are skipped due to ESM/fs-extra compatibility issues in test environment
// ============================================================================
describe('ISO 29119-4: Integration Tests', () => {
let projectPath: string;
let cleanup: () => void;
beforeEach(() => {
const temp = createTempProject();
projectPath = temp.projectPath;
cleanup = temp.cleanup;
});
afterEach(() => {
cleanup();
});
it('should pass validation for particle material followed by GPU particles', async () => {
// Test validation for particle material request
const materialResult = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/fire_mat.tres',
emissionShape: 'point',
direction: { x: 0, y: -1, z: 0 },
spread: 30,
initialVelocityMin: 20,
initialVelocityMax: 40,
color: { r: 1, g: 0.6, b: 0.1 },
});
expect(materialResult.content[0].text).toBeDefined();
// Test validation for GPU particles that reference it
const particlesResult = await handleCreateGPUParticles({
projectPath,
scenePath: 'scenes/main.tscn',
nodeName: 'FireEffect',
is3D: false,
amount: 100,
lifetime: 1.5,
materialPath: 'particles/fire_mat.tres',
});
expect(particlesResult.content[0].text).toBeDefined();
});
it('should pass validation for explosion effect with one-shot particles', async () => {
// Test validation for explosion material request
const materialResult = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/explosion_mat.tres',
emissionShape: 'sphere',
spread: 180,
initialVelocityMin: 50,
initialVelocityMax: 100,
gravity: { x: 0, y: 20, z: 0 },
scaleMin: 0.5,
scaleMax: 2.0,
});
expect(materialResult.content[0].text).toBeDefined();
// Test validation for one-shot explosion particles
const particlesResult = await handleCreateGPUParticles({
projectPath,
scenePath: 'scenes/main.tscn',
nodeName: 'ExplosionEffect',
is3D: false,
amount: 200,
lifetime: 0.5,
oneShot: true,
preprocess: 0.0,
materialPath: 'particles/explosion_mat.tres',
});
expect(particlesResult.content[0].text).toBeDefined();
});
it('should pass validation for multiple particle materials', async () => {
// Test validation for fire material
const fireResult = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/effects/fire.tres',
emissionShape: 'box',
color: { r: 1, g: 0.5, b: 0 },
});
expect(fireResult.content[0].text).toBeDefined();
// Test validation for smoke material
const smokeResult = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/effects/smoke.tres',
emissionShape: 'point',
gravity: { x: 0, y: -10, z: 0 },
color: { r: 0.3, g: 0.3, b: 0.3, a: 0.5 },
});
expect(smokeResult.content[0].text).toBeDefined();
// Test validation for spark material
const sparkResult = await handleCreateParticleMaterial({
projectPath,
materialPath: 'particles/effects/spark.tres',
emissionShape: 'sphere',
spread: 180,
initialVelocityMin: 100,
initialVelocityMax: 200,
scaleMin: 0.1,
scaleMax: 0.3,
color: { r: 1, g: 1, b: 0.5 },
});
expect(sparkResult.content[0].text).toBeDefined();
});
});
});