Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
run: dotnet run --project ClaudeRevit.McpTests -c Release
- name: Subscription CLI checks (isolated fake processes, no account)
run: dotnet run --project ClaudeRevit.CliTests -c Release
- name: ModPlus adapter checks (isolated Revit contracts, no installed ModPlus)
run: dotnet run --project ClaudeRevit.ModPlusTests -c Release
- name: Compile installer detection check
uses: Minionguyjpro/Inno-Setup-Action@v1.2.2
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ jobs:
if ($LASTEXITCODE -ne 0) { throw "MCP transport checks failed" }
dotnet run --project ClaudeRevit.CliTests -c Release
if ($LASTEXITCODE -ne 0) { throw "Subscription CLI checks failed" }
dotnet run --project ClaudeRevit.ModPlusTests -c Release
if ($LASTEXITCODE -ne 0) { throw "ModPlus adapter checks failed" }

# One add-in DLL cannot load into every Revit: 2025/2026 host .NET 8, 2027 hosts
# .NET 10. Build each supported version against its matching Revit API reference
Expand Down
17 changes: 17 additions & 0 deletions ClaudeRevit.ModPlusTests/ClaudeRevit.ModPlusTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable><RollForward>Major</RollForward>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Anthropic" Version="12.53.0" />
<Compile Include="../ClaudeRevit/Tools/ModPlusTools.cs" Link="src/ModPlusTools.cs" />
<Compile Include="../ClaudeRevit/Tools/IRevitTool.cs" Link="src/IRevitTool.cs" />
<Compile Include="../ClaudeRevit/Tools/ToolInput.cs" Link="src/ToolInput.cs" />
<Compile Include="../ClaudeRevit/Tools/ToolInputException.cs" Link="src/ToolInputException.cs" />
<Compile Include="../ClaudeRevit/Services/ModPlusCatalog.cs" Link="src/ModPlusCatalog.cs" />
<Compile Include="../ClaudeRevit/Services/ModPlusTotals.cs" Link="src/ModPlusTotals.cs" />
<Compile Include="../ClaudeRevit/Services/Json.cs" Link="src/Json.cs" />
<EmbeddedResource Include="../ClaudeRevit/Standards/ModPlus2027.json" LogicalName="ClaudeRevit.Standards.ModPlus2027.json" />
</ItemGroup>
</Project>
85 changes: 85 additions & 0 deletions ClaudeRevit.ModPlusTests/Contracts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Only Revit is replaced: the production tools, catalog, DTO reader and Anthropic schemas run
// unchanged. These contracts never ship in the add-in and cannot execute ModPlus assemblies.
using System.Text.Json;
using Anthropic.Models.Beta.Messages;
namespace Autodesk.Revit.DB
{
public sealed record ElementId(long Value);
public class Element { }
public class Document
{
public string Title => "isolated";
public Element? GetElement(ElementId id) => id.Value is 1 or 2 ? new Element() : null;
}
}
namespace Autodesk.Revit.ApplicationServices { public class Application { public string VersionNumber { get; set; } = "2027"; } }
namespace Autodesk.Revit.Exceptions { public class ArgumentException : System.ArgumentException { } }
namespace Autodesk.Revit.UI.Selection
{
public class Selection
{
private List<Autodesk.Revit.DB.ElementId> _ids = [new(1)];
public ICollection<Autodesk.Revit.DB.ElementId> GetElementIds() => _ids.ToArray();
public void SetElementIds(ICollection<Autodesk.Revit.DB.ElementId> ids) => _ids = ids.ToList();
}
}
namespace Autodesk.Revit.UI
{
public class UIDocument
{
public Autodesk.Revit.DB.Document Document { get; set; } = new();
public Selection.Selection Selection { get; } = new();
}
public class UIApplication
{
public Autodesk.Revit.ApplicationServices.Application Application { get; } = new();
public UIDocument? ActiveUIDocument { get; set; } = new();
public List<RibbonPanel> Panels { get; } = new();
public List<string> Posted { get; } = new();
public bool AllowPost { get; set; } = true;
public bool FailPost { get; set; }
public List<RibbonPanel> GetRibbonPanels(string tab) => tab == "ModPlus" ? Panels : [];
public bool CanPostCommand(RevitCommandId id) => AllowPost;
public void PostCommand(RevitCommandId id) { if(FailPost)throw new InvalidOperationException("Simulated posting failure."); Posted.Add(id.Name); }
}
public sealed record RevitCommandId(string Name) { public static RevitCommandId? LookupCommandId(string id) => string.IsNullOrEmpty(id) ? null : new(id); }
public class RibbonItem { public string Name { get; set; } = ""; public string ItemText { get; set; } = ""; }
public class PushButton : RibbonItem { public string ClassName { get; set; } = ""; }
public class RibbonPanel
{
public string Name { get; set; } = "Utilities";
public List<RibbonItem> Items { get; } = new();
public List<RibbonItem> GetItems() => Items;
}
public class PulldownButton : RibbonItem { public List<PushButton> Items { get; } = new(); public List<PushButton> GetItems() => Items; }
public class SplitButton : RibbonItem { public List<PushButton> Items { get; } = new(); public List<PushButton> GetItems() => Items; }
}
namespace ClaudeRevit.Services
{
public static class DocumentSessions { public static bool Same(Autodesk.Revit.DB.Document? a, Autodesk.Revit.DB.Document? b) => a != null && ReferenceEquals(a, b); }
}
namespace ClaudeRevit.Tools
{
public static class ToolDispatcher { public static bool ForceSuppress; }
public static class ToolContext
{
public static Autodesk.Revit.UI.UIDocument? Bound;
public static Autodesk.Revit.UI.UIDocument? UiDocument(Autodesk.Revit.UI.UIApplication app) => Bound ?? app.ActiveUIDocument;
public static void ThrowIfCancelled() { }
}
public static class NativeToolUtil
{
public static JsonElement Field(string type, string description) => JsonSerializer.SerializeToElement(new { type, description });
public static JsonElement Array(string type, string description) => JsonSerializer.SerializeToElement(new { type="array", items=new { type }, description });
public static InputSchema Schema(Dictionary<string, JsonElement> fields, params string[] required) => new() { Properties=fields, Required=required.ToList() };
public static string Text(IReadOnlyDictionary<string, JsonElement> input, string key, string fallback="") => input.TryGetValue(key,out var v) && v.ValueKind==JsonValueKind.String ? v.GetString()??fallback : fallback;
public static bool Preview(IReadOnlyDictionary<string, JsonElement> input) => !input.ContainsKey("preview") || !input.TryGetValue("preview",out var v) || v.ValueKind==JsonValueKind.Null || ToolInput.Flag(input,"preview");
public static List<Autodesk.Revit.DB.ElementId> Ids(JsonElement value,int max)
{
var ids=value.EnumerateArray().Select(x=>new Autodesk.Revit.DB.ElementId(x.GetInt64())).Distinct().ToList();
if(ids.Count<1||ids.Count>max)throw new ToolInputException("Invalid element_ids count.");
return ids;
}
public static Autodesk.Revit.DB.Element Element(Autodesk.Revit.DB.Document doc,long id) => doc.GetElement(new(id))??throw new ToolInputException("Missing element.");
}
}
72 changes: 72 additions & 0 deletions ClaudeRevit.ModPlusTests/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Text.Json;
using ClaudeRevit.Services;
using ClaudeRevit.Tools;
using Autodesk.Revit.UI;

var checks=0;
void Check(bool condition,string name) { if(!condition)throw new Exception(name); checks++; }
Dictionary<string,JsonElement> Args(string json)=>JsonSerializer.Deserialize<Dictionary<string,JsonElement>>(json)!;
JsonElement Call(IRevitTool tool,string json,UIApplication app)=>JsonSerializer.Deserialize<JsonElement>(tool.Execute(Args(json),app));
void Reject(Action action,string name) { try{action();}catch(ToolInputException){checks++;return;} throw new Exception("Not rejected: "+name); }
var data=Path.Combine(Path.GetTempPath(),"ClaudeRevit-ModPlus-contracts-"+Guid.NewGuid());
var previous=Environment.GetEnvironmentVariable("CLAUDEREVIT_MODPLUS_DATA");
Environment.SetEnvironmentVariable("CLAUDEREVIT_MODPLUS_DATA",data);
try
{
var app=new UIApplication();
var panel=new RibbonPanel();
var pull=new PulldownButton();
pull.Items.Add(new PushButton { Name="Sum", ItemText="Сумма параметров", ClassName="mprSumParameters.Command" });
panel.Items.Add(pull); app.Panels.Add(panel);
var list=new GetModPlusTools(); var algorithm=new GetModPlusAlgorithm(); var run=new RunModPlusCommand(); var sum=new ModPlusSumParameters();
foreach(var tool in new IRevitTool[]{list,algorithm,run,sum})
{
Check(!tool.RequiresTransaction,tool.Name+" transaction contract");
Check(!tool.RequiresCodeExecutionOptIn,tool.Name+" fixed native adapter");
var schema=JsonSerializer.SerializeToElement(tool.InputSchema);
Check(schema.GetProperty("type").GetString()=="object",tool.Name+" real MCP object schema");
}
var catalog=Call(list,"{\"module\":\"mprSumParameters\"}",app);
Check(catalog.GetProperty("modules")[0].GetProperty("installed")[0].GetProperty("className").GetString()=="mprSumParameters.Command","nested ribbon matching");
for(int offset=0;offset<86;offset+=10)
{
var result=list.Execute(Args("{\"offset\":"+offset+"}"),app);
Check(result.Length<40000,"catalog pages stay below transport cap");
}
var metadata=Call(algorithm,"{\"module\":\"mprSumParameters\",\"class_query\":\"Services\",\"method_query\":\"GetSelectElements\"}",app);
Check(!metadata.GetProperty("source_available").GetBoolean(),"metadata without local source");
Check(metadata.GetProperty("algorithmClasses")[0].GetProperty("methods")[0].GetProperty("name").GetString()=="GetSelectElements","public service mapping");
Directory.CreateDirectory(Path.Combine(data,"source"));
File.WriteAllLines(Path.Combine(data,"source","mprSumParameters_2027.decompiled.cs"),["// fixture one","// fixture two","// fixture three"]);
var excerpt=Call(algorithm,"{\"module\":\"mprSumParameters\",\"start_line\":2,\"line_count\":1}",app);
Check(excerpt.GetProperty("source").GetString()=="2: // fixture two","local source paging");
Reject(()=>algorithm.Execute(Args("{\"module\":\"../outside\"}"),app),"unknown module");
Reject(()=>algorithm.Execute(Args("{\"module\":\"mprSumParameters\",\"execute\":true}"),app),"executable argument");
Reject(()=>algorithm.Execute(Args("{\"module\":\"mprSumParameters\",\"line_count\":301}"),app),"source range");
var preview=Call(run,"{\"command\":\"mprSumParameters.Command\",\"element_ids\":[2]}",app);
Check(preview.GetProperty("preview").GetBoolean()&&!preview.GetProperty("posted").GetBoolean()&&app.Posted.Count==0,"preview never posts");
Check(app.ActiveUIDocument!.Selection.GetElementIds().Single().Value==1,"preview preserves selection");
Reject(()=>run.Execute(Args("{\"command\":\"mprSumParameters.Command\",\"command_id\":\"CustomCtrl_%CustomCtrl_%BIM-STARTER%Utilities%Sum\"}"),app),"foreign command ID");
Reject(()=>run.Execute(Args("{\"command\":\"mprSumParameters.Command\",\"element_ids\":[999]}"),app),"missing selection ID");
ToolDispatcher.ForceSuppress=true;
Reject(()=>run.Execute(Args("{\"command\":\"mprSumParameters.Command\",\"preview\":false}"),app),"unattended posting");
ToolDispatcher.ForceSuppress=false;
ToolContext.Bound=new UIDocument();
Reject(()=>run.Execute(Args("{\"command\":\"mprSumParameters.Command\"}"),app),"another document");
Reject(()=>sum.Execute(Args("{}"),app),"sum in another document");
ToolContext.Bound=null;
app.Application.VersionNumber="2026";
Reject(()=>sum.Execute(Args("{}"),app),"unsupported Revit execution");
Check(Call(list,"{}",app).GetProperty("currentRevit").GetString()=="2026","cross-version catalog inspection");
app.Application.VersionNumber="2027";
Reject(()=>sum.Execute(Args("{}"),app),"unloaded module");
app.FailPost=true;
try { run.Execute(Args("{\"command\":\"mprSumParameters.Command\",\"element_ids\":[2],\"preview\":false}"),app); throw new Exception("Expected posting failure."); }
catch(InvalidOperationException){ Check(app.ActiveUIDocument.Selection.GetElementIds().Single().Value==1,"failed posting restores selection"); }
app.FailPost=false;
var posted=Call(run,"{\"command\":\"mprSumParameters.Command\",\"element_ids\":[2],\"preview\":false}",app);
Check(posted.GetProperty("posted").GetBoolean()&&!posted.GetProperty("completed").GetBoolean()&&app.Posted.Count==1,"posted is not completed");
Check(app.ActiveUIDocument.Selection.GetElementIds().Single().Value==2,"selection passed to posted command");
Console.WriteLine(JsonSerializer.Serialize(new { checks, status="passed", nativeRevitExecution="not tested", scope="production tools with isolated Revit contracts and real Anthropic schemas" }));
}
finally { Environment.SetEnvironmentVariable("CLAUDEREVIT_MODPLUS_DATA",previous); ToolContext.Bound=null; ToolDispatcher.ForceSuppress=false; if(Directory.Exists(data))Directory.Delete(data,true); }
3 changes: 3 additions & 0 deletions ClaudeRevit.Tests/ClaudeRevit.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
<Compile Include="..\ClaudeRevit\Services\ToolResult.cs" Link="src\ToolResult.cs" />
<Compile Include="..\ClaudeRevit\Services\ToolResultCap.cs" Link="src\ToolResultCap.cs" />
<Compile Include="..\ClaudeRevit\Services\NormRules.cs" Link="src\NormRules.cs" />
<Compile Include="..\ClaudeRevit\Services\ModPlusCatalog.cs" Link="src\ModPlusCatalog.cs" />
<Compile Include="..\ClaudeRevit\Services\ModPlusTotals.cs" Link="src\ModPlusTotals.cs" />
<EmbeddedResource Include="..\ClaudeRevit\Standards\ModPlus2027.json" LogicalName="ClaudeRevit.Standards.ModPlus2027.json" />
<Compile Include="..\ClaudeRevit\Services\TepCalculator.cs" Link="src\TepCalculator.cs" />
<Compile Include="..\ClaudeRevit\Services\PdfDrawing.cs" Link="src\PdfDrawing.cs" />
<Compile Include="..\ClaudeRevit\Services\DwgShapes.cs" Link="src\DwgShapes.cs" />
Expand Down
Loading
Loading