Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,86 @@

3. In Revit: Go to **Add-ins** > **Settings** > **Refresh** > **Save**

## Testing

The test project uses [Nice3point.TUnit.Revit](https://github.com/Nice3point/RevitUnit) to run integration tests against a live Revit instance. No separate addin installation is required — the framework injects into the running Revit process automatically.

### Prerequisites

- **.NET 8 SDK** — install via `winget install Microsoft.DotNet.SDK.8`
- **Autodesk Revit 2026** — must be installed and licensed on your machine

### Running Tests

1. Open Revit 2026 and wait for it to fully load
2. Run the tests from the command line:

```bash
dotnet test -c Debug.R26 --project revit-mcp-commandset.Tests -r win-x64
```

> **Note:** The `-r win-x64` flag is required on ARM64 machines because the Revit API assemblies are x64-only.

Alternatively, you can use `dotnet run`:

```bash
cd revit-mcp-commandset.Tests
dotnet run -c Debug.R26
```

### Project Structure

| File | Purpose |
|------|---------|
| `revit-mcp-commandset.Tests/AssemblyInfo.cs` | Global `[assembly: TestExecutor<RevitThreadExecutor>]` — applies to all test methods, but hooks still need their own `[HookExecutor<RevitThreadExecutor>]` |
| `revit-mcp-commandset.Tests/Architecture/` | Tests for level and room creation commands |
| `revit-mcp-commandset.Tests/DataExtraction/` | Tests for model statistics, room data export, and material quantities |
| `revit-mcp-commandset.Tests/ColorSplashHandlerTests.cs` | Tests for color override functionality |
| `revit-mcp-commandset.Tests/TagRoomsHandlerTests.cs` | Tests for room tagging functionality |

### Writing New Tests

Test classes inherit from `RevitApiTest` and use TUnit's async assertion API. All assertions **must be awaited** — without `await`, assertions silently pass without checking anything.

Setup and cleanup hooks require `[HookExecutor<RevitThreadExecutor>]` to run on the Revit thread (the assembly-level `TestExecutor` only covers test methods, not hooks).

```csharp
public class MyTests : RevitApiTest
{
private static Document _doc = null!;
private static string _tempPath = null!;

[Before(HookType.Class)]
[HookExecutor<RevitThreadExecutor>]
public static void Setup()
{
var doc = Application.NewProjectDocument(UnitSystem.Imperial);
_tempPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid():N}.rvt");
doc.SaveAs(_tempPath);
doc.Close(false);
_doc = Application.OpenDocumentFile(_tempPath);
}

[After(HookType.Class)]
[HookExecutor<RevitThreadExecutor>]
public static void Cleanup()
{
_doc?.Close(false);
try { File.Delete(_tempPath); } catch { }
}

[Test]
public async Task MyTest_Condition_ExpectedResult()
{
var elements = new FilteredElementCollector(_doc)
.WhereElementIsNotElementType()
.ToElements();

await Assert.That(elements.Count).IsGreaterThan(0);
}
}
```

## Important Note

- Command names must be identical between `revit-mcp` and `revit-mcp-commandset` repositories, otherwise Claude cannot find them.
Expand Down
74 changes: 56 additions & 18 deletions command.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,51 +29,89 @@
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
"commandName": "create_Wall",
"description": "create a wall",
"commandName": "get_selected_elements",
"description": "get selected elements",
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
"commandName": "create_point_based_element",
"description": "Create point-based elements like furniture",
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
"commandName": "create_line_based_element",
"description": "Create line based element such as wall",
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
"commandName": "create_surface_based_element",
"description": "Create surface-based elements like floors",
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
"commandName": "color_splash",
"description": "Color elements by parameter value",
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
"commandName": "tag_walls",
"description": "Tag all the walls in the model",
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
"commandName": "delete_element",
"description": "Deletes elements using ElementId",
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
"commandName": "tag_all_walls",
"description": "Tag all the walls in the model",
{
"commandName": "ai_element_filter",
"description": "AI element filter for querying elements by criteria",
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
"commandName": "create_line_based_element",
"description": "Create line based element such as wall",
{
"commandName": "operate_element",
"description": "Operate on elements (select, color, hide, isolate, etc)",
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
{
"commandName": "export_room_data",
"description": "Extract all rooms with detailed properties including area, volume, perimeter, and parameters",
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
{
"commandName": "get_material_quantities",
"description": "Calculate material quantities and takeoffs with area and volume calculations per material",
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
{
"commandName": "analyze_model_statistics",
"description": "Analyze model complexity with element counts by category, type, family, and level",
"assemblyPath": "RevitMCPCommandSet.dll"
}
,
{
},
{
"commandName": "create_grid",
"description": "Create grid system with smart spacing generation for project layout (alphabetic/numeric naming)",
"assemblyPath": "RevitMCPCommandSet.dll"
}
,
{
},
{
"commandName": "create_structural_framing_system",
"description": "Create structural beam framing system with configurable spacing and direction",
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
"commandName": "create_room",
"description": "Create and place rooms at specified locations with custom names, numbers, and properties",
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
"commandName": "tag_rooms",
"description": "Create tags for all rooms in the current view displaying room name and number",
"assemblyPath": "RevitMCPCommandSet.dll"
},
{
"commandName": "create_level",
"description": "Create levels at specified elevations with automatic floor plan view generation",
"assemblyPath": "RevitMCPCommandSet.dll"
}
]
}
}
5 changes: 5 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"test": {
"runner": "Microsoft.Testing.Platform"
}
}
156 changes: 156 additions & 0 deletions revit-mcp-commandset.Tests/Architecture/CreateLevelHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
using Autodesk.Revit.DB;
using Nice3point.TUnit.Revit;
using Nice3point.TUnit.Revit.Executors;
using RevitMCPCommandSet.Models.Architecture;
using RevitMCPCommandSet.Services.Architecture;
using TUnit.Core;
using TUnit.Core.Executors;

namespace RevitMCPCommandSet.Tests.Architecture;

public class CreateLevelHandlerTests : RevitApiTest
{
private static Document _doc = null!;
private static string _tempPath = null!;

[Before(HookType.Class)]
[HookExecutor<RevitThreadExecutor>]
public static void Setup()
{
var doc = Application.NewProjectDocument(UnitSystem.Imperial);
_tempPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid():N}.rvt");
doc.SaveAs(_tempPath);
doc.Close(false);
_doc = Application.OpenDocumentFile(_tempPath);
}

[After(HookType.Class)]
[HookExecutor<RevitThreadExecutor>]
public static void Cleanup()
{
_doc?.Close(false);
try { File.Delete(_tempPath); } catch { }
}

[Test]
[TestExecutor<RevitThreadExecutor>]
public async Task Execute_SingleLevel_CreatesLevelAtCorrectElevation()
{
var handler = new CreateLevelHandler();
handler.SetParameters(new List<LevelInfo>
{
new LevelInfo { Name = "Single Level Test", Elevation = 3000 }
});

handler.RunOnDocument(_doc);

await Assert.That(handler.Result).IsNotNull();
await Assert.That(handler.Result.Success).IsTrue();
await Assert.That(handler.Result.Response).IsNotNull();
await Assert.That(handler.Result.Response.Count).IsEqualTo(1);
await Assert.That(handler.Result.Response[0].Elevation).IsEqualTo(3000);
await Assert.That(handler.Result.Response[0].AlreadyExisted).IsFalse();
}

[Test]
[TestExecutor<RevitThreadExecutor>]
public async Task Execute_SetName_LevelNameApplied()
{
var handler = new CreateLevelHandler();
handler.SetParameters(new List<LevelInfo>
{
new LevelInfo { Name = "Custom Name Level", Elevation = 5000 }
});

handler.RunOnDocument(_doc);

await Assert.That(handler.Result.Success).IsTrue();
await Assert.That(handler.Result.Response[0].Name).IsEqualTo("Custom Name Level");
}

[Test]
[TestExecutor<RevitThreadExecutor>]
public async Task Execute_WithFloorPlan_FloorPlanViewCreated()
{
var handler = new CreateLevelHandler();
handler.SetParameters(new List<LevelInfo>
{
new LevelInfo { Name = "FloorPlan Level", Elevation = 7000, CreateFloorPlan = true, CreateCeilingPlan = false }
});

handler.RunOnDocument(_doc);

await Assert.That(handler.Result.Success).IsTrue();
await Assert.That(handler.Result.Response[0].FloorPlanViewName).IsNotNull();
await Assert.That(handler.Result.Response[0].CeilingPlanViewName).IsNull();
}

[Test]
[TestExecutor<RevitThreadExecutor>]
public async Task Execute_WithCeilingPlan_CeilingPlanViewCreated()
{
var handler = new CreateLevelHandler();
handler.SetParameters(new List<LevelInfo>
{
new LevelInfo { Name = "CeilingPlan Level", Elevation = 9000, CreateFloorPlan = false, CreateCeilingPlan = true }
});

handler.RunOnDocument(_doc);

await Assert.That(handler.Result.Success).IsTrue();
await Assert.That(handler.Result.Response[0].CeilingPlanViewName).IsNotNull();
await Assert.That(handler.Result.Response[0].FloorPlanViewName).IsNull();
}

[Test]
[TestExecutor<RevitThreadExecutor>]
public async Task Execute_DuplicateName_ReportsAlreadyExisted()
{
// First call: create the level
var handler1 = new CreateLevelHandler();
handler1.SetParameters(new List<LevelInfo>
{
new LevelInfo { Name = "Dup Test Level", Elevation = 11000 }
});
handler1.RunOnDocument(_doc);
await Assert.That(handler1.Result.Success).IsTrue();
await Assert.That(handler1.Result.Response[0].AlreadyExisted).IsFalse();

// Second call: same name should report already existed
var handler2 = new CreateLevelHandler();
handler2.SetParameters(new List<LevelInfo>
{
new LevelInfo { Name = "Dup Test Level", Elevation = 12000 }
});
handler2.RunOnDocument(_doc);

await Assert.That(handler2.Result.Success).IsTrue();
await Assert.That(handler2.Result.Response[0].AlreadyExisted).IsTrue();
}

[Test]
[TestExecutor<RevitThreadExecutor>]
public async Task Execute_MultipleLevels_AllCreatedAtCorrectElevations()
{
var levels = new List<LevelInfo>
{
new LevelInfo { Name = "Batch A", Elevation = 0 },
new LevelInfo { Name = "Batch B", Elevation = 3000 },
new LevelInfo { Name = "Batch C", Elevation = 6000 },
new LevelInfo { Name = "Batch D", Elevation = 9000 }
};

var handler = new CreateLevelHandler();
handler.SetParameters(levels);
handler.RunOnDocument(_doc);

await Assert.That(handler.Result.Success).IsTrue();
await Assert.That(handler.Result.Response.Count).IsEqualTo(4);

for (int i = 0; i < levels.Count; i++)
{
await Assert.That(handler.Result.Response[i].Name).IsEqualTo(levels[i].Name);
await Assert.That(handler.Result.Response[i].Elevation).IsEqualTo(levels[i].Elevation);
}
}
}
Loading