/**
* Physics Tools Tests
* ISO/IEC 29119 compliant - comprehensive test coverage
* ISO/IEC 25010 compliant - strict testing
*
* Tests for:
* - ConfigurePhysicsLayersTool
* - CreateCollisionShapeTool
* - SetupRigidBodyTool
*/
import { readFileSync } from 'fs';
import { join } from 'path';
import { handleCreateCollisionShape } from './CreateCollisionShapeTool';
import { handleSetupRigidBody } from './SetupRigidBodyTool';
import { handleConfigurePhysicsLayers } from './ConfigurePhysicsLayersTool';
import {
createTempProject,
getResponseText,
isErrorResponse,
} from '../test-utils.js';
describe('Physics Tools', () => {
// ============================================================================
// ConfigurePhysicsLayers Tests
// ============================================================================
describe('ConfigurePhysicsLayers', () => {
describe('ISO 29119-4: Input Validation', () => {
describe('Required Parameter Validation', () => {
it('should return error when projectPath is missing', async () => {
const result = await handleConfigurePhysicsLayers({
dimension: '2d',
layers: [{ layer: 1, name: 'Player' }],
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('projectPath');
});
it('should return error when dimension is missing', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath: '/path/to/project',
layers: [{ layer: 1, name: 'Player' }],
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('dimension');
});
it('should return error when layers is missing', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath: '/path/to/project',
dimension: '2d',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('layers');
});
it('should return error for empty projectPath', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath: '',
dimension: '2d',
layers: [{ layer: 1, name: 'Player' }],
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/projectPath|empty/i);
});
});
describe('Enum Validation', () => {
it('should return error for invalid dimension value', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath: '/path/to/project',
dimension: 'invalid' as '2d',
layers: [{ layer: 1, name: 'Player' }],
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/dimension|Invalid input/i);
});
it('should accept 2d dimension', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath: '/non/existent/path',
dimension: '2d',
layers: [{ layer: 1, name: 'Player' }],
});
// Should pass enum validation, fail on project validation
expect(result.isError).toBe(true);
expect(result.content[0].text).not.toContain('dimension');
});
it('should accept 3d dimension', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath: '/non/existent/path',
dimension: '3d',
layers: [{ layer: 1, name: 'Player' }],
});
// Should pass enum validation, fail on project validation
expect(result.isError).toBe(true);
expect(result.content[0].text).not.toContain('dimension');
});
});
describe('Layers Array Validation', () => {
it('should return error for empty layers array', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath: '/non/existent/path',
dimension: '2d',
layers: [],
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/layers|Too small/i);
});
it('should return error for layer number less than 1', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath: '/non/existent/path',
dimension: '2d',
layers: [{ layer: 0, name: 'Invalid' }],
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/layer|Too small/i);
});
it('should return error for layer number greater than 32', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath: '/non/existent/path',
dimension: '2d',
layers: [{ layer: 33, name: 'Invalid' }],
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/layer|Too big/i);
});
it('should return error for layer without name', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath: '/non/existent/path',
dimension: '2d',
layers: [{ layer: 1, name: '' }],
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/name|Too small/i);
});
it('should accept layer at boundary value 1', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath: '/non/existent/path',
dimension: '2d',
layers: [{ layer: 1, name: 'First' }],
});
expect(result.isError).toBe(true);
// Error should be project-related, not layer-related
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept layer at boundary value 32', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath: '/non/existent/path',
dimension: '2d',
layers: [{ layer: 32, name: 'Last' }],
});
expect(result.isError).toBe(true);
// Error should be project-related, not layer-related
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept multiple layers', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath: '/non/existent/path',
dimension: '2d',
layers: [
{ layer: 1, name: 'Player' },
{ layer: 2, name: 'Enemies' },
{ layer: 3, name: 'Ground' },
],
});
expect(result.isError).toBe(true);
// Error should be project-related
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 handleConfigurePhysicsLayers({
projectPath: '/path/../../../etc/passwd',
dimension: '2d',
layers: [{ layer: 1, name: 'Player' }],
});
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);
});
});
describe('Project Validation', () => {
it('should return error for non-existent project path', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath: '/non/existent/path',
dimension: '2d',
layers: [{ layer: 1, name: 'Player' }],
});
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 configure 2D physics layers successfully', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath,
dimension: '2d',
layers: [
{ layer: 1, name: 'Player' },
{ layer: 2, name: 'Enemies' },
],
});
// Note: Actual execution may fail due to Godot not being available
// but we verify the validation passes
expect(result.content[0].text).toBeDefined();
});
it('should configure 3D physics layers successfully', async () => {
const result = await handleConfigurePhysicsLayers({
projectPath,
dimension: '3d',
layers: [
{ layer: 1, name: 'World' },
{ layer: 2, name: 'Characters' },
],
});
expect(result.content[0].text).toBeDefined();
});
});
});
// ============================================================================
// CreateCollisionShape Tests
// ============================================================================
describe('CreateCollisionShape', () => {
describe('ISO 29119-4: Input Validation', () => {
describe('Required Parameter Validation', () => {
it('should return error when projectPath is missing', async () => {
const result = await handleCreateCollisionShape({
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'rectangle',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('projectPath');
});
it('should return error when scenePath is missing', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/path/to/project',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'rectangle',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('scenePath');
});
it('should return error when shapeType is missing', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/path/to/project',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('shapeType');
});
it('should return error when parentNodePath is missing', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/path/to/project',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
shapeType: 'rectangle',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('parentNodePath');
});
it('should return error for empty projectPath', async () => {
const result = await handleCreateCollisionShape({
projectPath: '',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'rectangle',
});
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 handleCreateCollisionShape({
projectPath: '/path/to/project',
scenePath: '',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'rectangle',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/scenePath|empty/i);
});
it('should return error for empty parentNodePath', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/path/to/project',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: '',
shapeType: 'rectangle',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/parentNodePath|empty/i);
});
});
describe('Shape Type Enum Validation', () => {
it('should return error for invalid shapeType enum', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/path/to/project',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'invalid' as 'rectangle',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/shapeType|Invalid input/i);
});
it('should return error for invalid 2D shape type (box is 3D only)', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'box',
is3D: false,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Invalid 2D shape');
});
it('should return error for invalid 2D shape type (sphere is 3D only)', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'sphere',
is3D: false,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Invalid 2D shape');
});
it('should return error for invalid 2D shape type (cylinder is 3D only)', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'cylinder',
is3D: false,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Invalid 2D shape');
});
it('should return error for invalid 2D shape type (convex is 3D only)', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'convex',
is3D: false,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Invalid 2D shape');
});
it('should return error for invalid 3D shape type (rectangle is 2D only)', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'rectangle',
is3D: true,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Invalid 3D shape');
});
it('should return error for invalid 3D shape type (circle is 2D only)', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'circle',
is3D: true,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Invalid 3D shape');
});
it('should return error for invalid 3D shape type (polygon is 2D only)', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'polygon',
is3D: true,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Invalid 3D shape');
});
it('should accept valid 2D rectangle shape', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'rectangle',
is3D: false,
});
expect(result.isError).toBe(true);
// Should fail on project validation, not shape validation
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept valid 2D circle shape', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'circle',
is3D: false,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept valid 2D capsule shape', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'capsule',
is3D: false,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept valid 2D polygon shape', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'polygon',
is3D: false,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept valid 3D box shape', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'box',
is3D: true,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept valid 3D sphere shape', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'sphere',
is3D: true,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept valid 3D capsule shape (shared between 2D and 3D)', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'capsule',
is3D: true,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept valid 3D cylinder shape', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'cylinder',
is3D: true,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept valid 3D convex shape', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'convex',
is3D: true,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
});
describe('Shape Parameters Validation', () => {
it('should accept shape with radius parameter', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'circle',
is3D: false,
shapeParams: { radius: 50 },
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept shape with size parameters', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'rectangle',
is3D: false,
shapeParams: { sizeX: 100, sizeY: 50 },
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept 3D shape with full size parameters', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'box',
is3D: true,
shapeParams: { sizeX: 1, sizeY: 2, sizeZ: 3 },
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept capsule shape with height and radius', async () => {
const result = await handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'capsule',
is3D: false,
shapeParams: { height: 100, radius: 25 },
});
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 handleCreateCollisionShape({
projectPath: '/path/../../../etc/passwd',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'rectangle',
});
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 handleCreateCollisionShape({
projectPath: '/path/to/project',
scenePath: '../../../etc/passwd',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'rectangle',
});
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 handleCreateCollisionShape({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'rectangle',
is3D: false,
});
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 handleCreateCollisionShape({
projectPath,
scenePath: 'scenes/nonexistent.tscn',
nodeName: 'Collision',
parentNodePath: 'Player',
shapeType: 'rectangle',
is3D: false,
});
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 valid 2D collision shape request', async () => {
const result = await handleCreateCollisionShape({
projectPath,
scenePath: 'scenes/main.tscn',
nodeName: 'PlayerCollision',
parentNodePath: 'Player',
shapeType: 'rectangle',
is3D: false,
});
// Validation should pass (may fail on Godot execution)
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for valid 3D collision shape request', async () => {
const result = await handleCreateCollisionShape({
projectPath,
scenePath: 'scenes/main.tscn',
nodeName: 'PlayerCollision3D',
parentNodePath: 'Player',
shapeType: 'box',
is3D: true,
});
expect(result.content[0].text).toBeDefined();
});
});
});
// ============================================================================
// SetupRigidBody Tests
// ============================================================================
describe('SetupRigidBody', () => {
describe('ISO 29119-4: Input Validation', () => {
describe('Required Parameter Validation', () => {
it('should return error when projectPath is missing', async () => {
const result = await handleSetupRigidBody({
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('projectPath');
});
it('should return error when scenePath is missing', async () => {
const result = await handleSetupRigidBody({
projectPath: '/path/to/project',
nodePath: 'Player',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('scenePath');
});
it('should return error when nodePath is missing', async () => {
const result = await handleSetupRigidBody({
projectPath: '/path/to/project',
scenePath: 'scenes/main.tscn',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('nodePath');
});
it('should return error for empty projectPath', async () => {
const result = await handleSetupRigidBody({
projectPath: '',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
});
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 handleSetupRigidBody({
projectPath: '/path/to/project',
scenePath: '',
nodePath: 'Player',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/scenePath|empty/i);
});
it('should return error for empty nodePath', async () => {
const result = await handleSetupRigidBody({
projectPath: '/path/to/project',
scenePath: 'scenes/main.tscn',
nodePath: '',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/nodePath|empty/i);
});
});
describe('Body Type Enum Validation', () => {
it('should return error for invalid bodyType enum', async () => {
const result = await handleSetupRigidBody({
projectPath: '/path/to/project',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
bodyType: 'invalid' as 'dynamic',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/bodyType|Invalid input/i);
});
it('should accept dynamic bodyType', async () => {
const result = await handleSetupRigidBody({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
bodyType: 'dynamic',
});
expect(result.isError).toBe(true);
// Should fail on project validation, not bodyType validation
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept static bodyType', async () => {
const result = await handleSetupRigidBody({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
bodyType: 'static',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept kinematic bodyType', async () => {
const result = await handleSetupRigidBody({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
bodyType: 'kinematic',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
});
describe('Physics Properties Validation', () => {
it('should accept valid mass value', async () => {
const result = await handleSetupRigidBody({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
mass: 5.0,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept valid gravity_scale value', async () => {
const result = await handleSetupRigidBody({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
gravity_scale: 2.0,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept zero gravity_scale', async () => {
const result = await handleSetupRigidBody({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
gravity_scale: 0,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept negative gravity_scale', async () => {
const result = await handleSetupRigidBody({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
gravity_scale: -1.0,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept valid linear_damp value', async () => {
const result = await handleSetupRigidBody({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
linear_damp: 0.5,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept valid angular_damp value', async () => {
const result = await handleSetupRigidBody({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
angular_damp: 0.5,
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept physics_material path', async () => {
const result = await handleSetupRigidBody({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
physics_material: 'materials/bouncy.tres',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Not a valid Godot project');
});
it('should accept multiple physics properties together', async () => {
const result = await handleSetupRigidBody({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
bodyType: 'dynamic',
mass: 10.0,
gravity_scale: 1.5,
linear_damp: 0.1,
angular_damp: 0.2,
});
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 handleSetupRigidBody({
projectPath: '/path/../../../etc/passwd',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
});
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 handleSetupRigidBody({
projectPath: '/path/to/project',
scenePath: '../../../etc/passwd',
nodePath: 'Player',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toMatch(/path traversal|\.\./i);
});
it('should return error for path traversal in physics_material', async () => {
const result = await handleSetupRigidBody({
projectPath: '/path/to/project',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
physics_material: '../../../etc/passwd',
});
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 handleSetupRigidBody({
projectPath: '/non/existent/path',
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
});
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 handleSetupRigidBody({
projectPath,
scenePath: 'scenes/nonexistent.tscn',
nodePath: 'Player',
});
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 valid RigidBody setup request', async () => {
const result = await handleSetupRigidBody({
projectPath,
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
bodyType: 'dynamic',
mass: 1.0,
});
// Validation should pass (may fail on Godot execution)
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for static body setup', async () => {
const result = await handleSetupRigidBody({
projectPath,
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
bodyType: 'static',
});
expect(result.content[0].text).toBeDefined();
});
it('should pass validation for kinematic body setup', async () => {
const result = await handleSetupRigidBody({
projectPath,
scenePath: 'scenes/main.tscn',
nodePath: 'Player',
bodyType: 'kinematic',
});
expect(result.content[0].text).toBeDefined();
});
});
});
});