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
28 changes: 28 additions & 0 deletions .github/workflows/ci-dotnet-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: .NET CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '7.0.x'

- name: Restore dependencies
run: dotnet restore SmartHopper.sln

- name: Build solution
run: dotnet build --no-restore --configuration Release SmartHopper.sln

- name: Run all tests
run: dotnet test --no-build --configuration Release --results-directory TestResults
Comment on lines +11 to +28

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
13 changes: 13 additions & 0 deletions .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@ jobs:
- name: Build solution
run: dotnet build SmartHopper.sln --configuration Release --no-restore /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=signing.snk

- name: Decode Authenticode PFX from secrets
shell: pwsh
run: |
Write-Host "Decoding Authenticode PFX from secrets..."
pwsh ./Sign-Authenticode.ps1 -Base64 "${{ secrets.SIGN_PFX_BASE64 }}" -Password "${{ secrets.SIGN_PFX_PASSWORD }}"

- name: Authenticode-sign provider assemblies
shell: pwsh
run: |
$version = "${{ steps.determine_version.outputs.VERSION }}"
Write-Host "Auth-signing SmartHopper provider DLLs in bin\\$version\\Release"
pwsh ./Sign-Authenticode.ps1 -Sign "bin\\$version\\Release" -Password "${{ secrets.SIGN_PFX_PASSWORD }}"

- name: Create Output Directory
shell: pwsh
run: |
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added new `ghmoveobj` AI tool in `GhObjTools` for moving Grasshopper component pivot by GUID with absolute or relative position.
- Added `MoveInstance` method in `GHCanvasUtils` to move existing instances by GUID with absolute or relative pivot positions.
- Improved security in Providers by accepting only signed assemblies.
- Added multiple CI Tests, for example,to ensure unsigned provider assemblies are rejected by `ProviderManager.VerifySignature`, to ensure only signed assemblies are loaded by `ProviderManager.LoadProviderAssembly`, and to ensure only enabled providers are registered by `ProviderManager.RegisterProviders`.
- Added `AIToolCall.cs`, a new model for AI tool call requests.
- Added `SmartHopperInitializer.cs`, a static class for safe startup and provider initialization.

Expand All @@ -45,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed mismatch between in-memory and on-disk `TrustedProviders` when prompting in `ProviderManager.LoadProviderAssembly()`
- Fixed a bug in `DataProcessor` where results were being duplicated when multiple branches were grouped together to unsuccessfully prevent unnecessary API calls [#32](https://github.com/architects-toolkit/SmartHopper/issues/32)
- Fixed inconsistent list format handling between `AIListEvaluate` and `AIListFilter` components.
- Fixed `MistralAI` provider not loading `AI Tools`.

## [0.2.0-alpha] - 2025-04-06

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# SmartHopper - AI-Powered Grasshopper3D Plugin

[![Version](https://img.shields.io/badge/version-0%2E3%2E0--dev%2E250421-brown)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Version](https://img.shields.io/badge/version-0%2E3%2E0--dev%2E250423-brown)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Status](https://img.shields.io/badge/status-Unstable%20Development-brown)](https://github.com/architects-toolkit/SmartHopper/releases)
[![.NET CI](https://github.com/architects-toolkit/SmartHopper/actions/workflows/ci-dotnet-tests.yml/badge.svg)](https://github.com/architects-toolkit/SmartHopper/actions/workflows/ci-dotnet-tests.yml)
[![Grasshopper](https://img.shields.io/badge/plugin_for-Grasshopper3D-darkgreen?logo=rhinoceros)](https://www.rhino3d.com/)
[![MistralAI](https://img.shields.io/badge/AI--powered-MistralAI-orange)](https://mistral.ai/)
[![OpenAI](https://img.shields.io/badge/AI--powered-OpenAI-blue?logo=openai)](https://openai.com/)
Expand Down
14 changes: 11 additions & 3 deletions Sign-Authenticode.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
Authenticode-signs all SmartHopper.Providers.*.dll under the given path, using the decoded PFX.
.PARAMETER Help
Displays this help message.
.PARAMETER PfxPath
Override default signing PFX path (default 'signing.pfx').
#>
param(
[switch]$Generate,
Expand All @@ -26,10 +28,12 @@ param(
[string]$Password,
[switch]$Export,
[switch]$Help,
[string]$Sign
[string]$Sign,
[string]$PfxPath
)

$pfxPath = 'signing.pfx'
# default PFX path; override via -PfxPath
$pfxPath = if ($PfxPath) { $PfxPath } else { 'signing.pfx' }

function Show-Help {
Write-Host "Usage: .\Sign-Authenticode.ps1 [options]"
Expand All @@ -42,6 +46,7 @@ function Show-Help {
Write-Host " -Export Exports signing.pfx as Base64 text to stdout. Requires -Password."
Write-Host " -Sign <path> Authenticode-signs all SmartHopper.Providers.*.dll under <path>. Requires Base64 or Generate."
Write-Host " -Help Displays this help message."
Write-Host " -PfxPath <path> Override default signing PFX path (default 'signing.pfx')."
}

if ($Help -or (-not $Generate -and -not $Base64 -and -not $File -and -not $Export -and -not $Sign)) {
Expand Down Expand Up @@ -125,7 +130,10 @@ if ($Generate) {

# Attempt file-based signing first
Write-Host "Attempting file-based signing..."
Get-ChildItem -Path $Sign -Recurse -Filter "SmartHopper.Providers.*.dll" | ForEach-Object {
# Sign provider DLLs and the Config assembly
Get-ChildItem -Path $Sign -Recurse -Filter "*.dll" |
Where-Object { $_.Name -like "SmartHopper.Providers.*.dll" -or $_.Name -eq "SmartHopper.Config.dll" } |
ForEach-Object {
$dll = $_.FullName
Write-Host "Signing $dll with PFX file..."
& $signtool.Source sign /fd SHA256 /a /f "$pfxPath" /p "$Password" $dll
Expand Down
85 changes: 85 additions & 0 deletions SmartHopper.sln
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
Expand All @@ -18,43 +19,127 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Providers.Mistr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Providers.OpenAI", "src\SmartHopper.Providers.OpenAI\SmartHopper.Providers.OpenAI.csproj", "{087FFA5E-1049-459D-9C68-1C0B8E7F9EBC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Config.Tests", "src\SmartHopper.Config.Tests\SmartHopper.Config.Tests.csproj", "{469B4BA9-C2CB-4270-8896-0F04DAF67887}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D4C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D4C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E1}.Debug|x64.ActiveCfg = Debug|Any CPU
{D4C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E1}.Debug|x64.Build.0 = Debug|Any CPU
{D4C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E1}.Debug|x86.ActiveCfg = Debug|Any CPU
{D4C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E1}.Debug|x86.Build.0 = Debug|Any CPU
{D4C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E1}.Release|Any CPU.Build.0 = Release|Any CPU
{D4C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E1}.Release|x64.ActiveCfg = Release|Any CPU
{D4C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E1}.Release|x64.Build.0 = Release|Any CPU
{D4C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E1}.Release|x86.ActiveCfg = Release|Any CPU
{D4C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E1}.Release|x86.Build.0 = Release|Any CPU
{D5C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D5C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D5C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E5}.Debug|x64.ActiveCfg = Debug|Any CPU
{D5C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E5}.Debug|x64.Build.0 = Debug|Any CPU
{D5C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E5}.Debug|x86.ActiveCfg = Debug|Any CPU
{D5C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E5}.Debug|x86.Build.0 = Debug|Any CPU
{D5C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D5C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E5}.Release|Any CPU.Build.0 = Release|Any CPU
{D5C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E5}.Release|x64.ActiveCfg = Release|Any CPU
{D5C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E5}.Release|x64.Build.0 = Release|Any CPU
{D5C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E5}.Release|x86.ActiveCfg = Release|Any CPU
{D5C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E5}.Release|x86.Build.0 = Release|Any CPU
{60732624-037C-4D6D-BC4C-588A0C25C338}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{60732624-037C-4D6D-BC4C-588A0C25C338}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60732624-037C-4D6D-BC4C-588A0C25C338}.Debug|x64.ActiveCfg = Debug|Any CPU
{60732624-037C-4D6D-BC4C-588A0C25C338}.Debug|x64.Build.0 = Debug|Any CPU
{60732624-037C-4D6D-BC4C-588A0C25C338}.Debug|x86.ActiveCfg = Debug|Any CPU
{60732624-037C-4D6D-BC4C-588A0C25C338}.Debug|x86.Build.0 = Debug|Any CPU
{60732624-037C-4D6D-BC4C-588A0C25C338}.Release|Any CPU.ActiveCfg = Release|Any CPU
{60732624-037C-4D6D-BC4C-588A0C25C338}.Release|Any CPU.Build.0 = Release|Any CPU
{60732624-037C-4D6D-BC4C-588A0C25C338}.Release|x64.ActiveCfg = Release|Any CPU
{60732624-037C-4D6D-BC4C-588A0C25C338}.Release|x64.Build.0 = Release|Any CPU
{60732624-037C-4D6D-BC4C-588A0C25C338}.Release|x86.ActiveCfg = Release|Any CPU
{60732624-037C-4D6D-BC4C-588A0C25C338}.Release|x86.Build.0 = Release|Any CPU
{A932CFFA-0C82-4A1F-92F2-003CDE1C94AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A932CFFA-0C82-4A1F-92F2-003CDE1C94AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A932CFFA-0C82-4A1F-92F2-003CDE1C94AC}.Debug|x64.ActiveCfg = Debug|Any CPU
{A932CFFA-0C82-4A1F-92F2-003CDE1C94AC}.Debug|x64.Build.0 = Debug|Any CPU
{A932CFFA-0C82-4A1F-92F2-003CDE1C94AC}.Debug|x86.ActiveCfg = Debug|Any CPU
{A932CFFA-0C82-4A1F-92F2-003CDE1C94AC}.Debug|x86.Build.0 = Debug|Any CPU
{A932CFFA-0C82-4A1F-92F2-003CDE1C94AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A932CFFA-0C82-4A1F-92F2-003CDE1C94AC}.Release|Any CPU.Build.0 = Release|Any CPU
{A932CFFA-0C82-4A1F-92F2-003CDE1C94AC}.Release|x64.ActiveCfg = Release|Any CPU
{A932CFFA-0C82-4A1F-92F2-003CDE1C94AC}.Release|x64.Build.0 = Release|Any CPU
{A932CFFA-0C82-4A1F-92F2-003CDE1C94AC}.Release|x86.ActiveCfg = Release|Any CPU
{A932CFFA-0C82-4A1F-92F2-003CDE1C94AC}.Release|x86.Build.0 = Release|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AD}.Debug|x64.ActiveCfg = Debug|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AD}.Debug|x64.Build.0 = Debug|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AD}.Debug|x86.ActiveCfg = Debug|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AD}.Debug|x86.Build.0 = Debug|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AD}.Release|Any CPU.Build.0 = Release|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AD}.Release|x64.ActiveCfg = Release|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AD}.Release|x64.Build.0 = Release|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AD}.Release|x86.ActiveCfg = Release|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AD}.Release|x86.Build.0 = Release|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AE}.Debug|x64.ActiveCfg = Debug|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AE}.Debug|x64.Build.0 = Debug|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AE}.Debug|x86.ActiveCfg = Debug|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AE}.Debug|x86.Build.0 = Debug|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AE}.Release|x64.ActiveCfg = Release|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AE}.Release|x64.Build.0 = Release|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AE}.Release|x86.ActiveCfg = Release|Any CPU
{B932CFFA-0C82-4A1F-92F2-003CDE1C94AE}.Release|x86.Build.0 = Release|Any CPU
{F2063A0F-FDF2-4AD2-8C92-1E374B991752}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2063A0F-FDF2-4AD2-8C92-1E374B991752}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2063A0F-FDF2-4AD2-8C92-1E374B991752}.Debug|x64.ActiveCfg = Debug|Any CPU
{F2063A0F-FDF2-4AD2-8C92-1E374B991752}.Debug|x64.Build.0 = Debug|Any CPU
{F2063A0F-FDF2-4AD2-8C92-1E374B991752}.Debug|x86.ActiveCfg = Debug|Any CPU
{F2063A0F-FDF2-4AD2-8C92-1E374B991752}.Debug|x86.Build.0 = Debug|Any CPU
{F2063A0F-FDF2-4AD2-8C92-1E374B991752}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2063A0F-FDF2-4AD2-8C92-1E374B991752}.Release|Any CPU.Build.0 = Release|Any CPU
{F2063A0F-FDF2-4AD2-8C92-1E374B991752}.Release|x64.ActiveCfg = Release|Any CPU
{F2063A0F-FDF2-4AD2-8C92-1E374B991752}.Release|x64.Build.0 = Release|Any CPU
{F2063A0F-FDF2-4AD2-8C92-1E374B991752}.Release|x86.ActiveCfg = Release|Any CPU
{F2063A0F-FDF2-4AD2-8C92-1E374B991752}.Release|x86.Build.0 = Release|Any CPU
{087FFA5E-1049-459D-9C68-1C0B8E7F9EBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{087FFA5E-1049-459D-9C68-1C0B8E7F9EBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{087FFA5E-1049-459D-9C68-1C0B8E7F9EBC}.Debug|x64.ActiveCfg = Debug|Any CPU
{087FFA5E-1049-459D-9C68-1C0B8E7F9EBC}.Debug|x64.Build.0 = Debug|Any CPU
{087FFA5E-1049-459D-9C68-1C0B8E7F9EBC}.Debug|x86.ActiveCfg = Debug|Any CPU
{087FFA5E-1049-459D-9C68-1C0B8E7F9EBC}.Debug|x86.Build.0 = Debug|Any CPU
{087FFA5E-1049-459D-9C68-1C0B8E7F9EBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{087FFA5E-1049-459D-9C68-1C0B8E7F9EBC}.Release|Any CPU.Build.0 = Release|Any CPU
{087FFA5E-1049-459D-9C68-1C0B8E7F9EBC}.Release|x64.ActiveCfg = Release|Any CPU
{087FFA5E-1049-459D-9C68-1C0B8E7F9EBC}.Release|x64.Build.0 = Release|Any CPU
{087FFA5E-1049-459D-9C68-1C0B8E7F9EBC}.Release|x86.ActiveCfg = Release|Any CPU
{087FFA5E-1049-459D-9C68-1C0B8E7F9EBC}.Release|x86.Build.0 = Release|Any CPU
{469B4BA9-C2CB-4270-8896-0F04DAF67887}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{469B4BA9-C2CB-4270-8896-0F04DAF67887}.Debug|Any CPU.Build.0 = Debug|Any CPU
{469B4BA9-C2CB-4270-8896-0F04DAF67887}.Debug|x64.ActiveCfg = Debug|Any CPU
{469B4BA9-C2CB-4270-8896-0F04DAF67887}.Debug|x64.Build.0 = Debug|Any CPU
{469B4BA9-C2CB-4270-8896-0F04DAF67887}.Debug|x86.ActiveCfg = Debug|Any CPU
{469B4BA9-C2CB-4270-8896-0F04DAF67887}.Debug|x86.Build.0 = Debug|Any CPU
{469B4BA9-C2CB-4270-8896-0F04DAF67887}.Release|Any CPU.ActiveCfg = Release|Any CPU
{469B4BA9-C2CB-4270-8896-0F04DAF67887}.Release|Any CPU.Build.0 = Release|Any CPU
{469B4BA9-C2CB-4270-8896-0F04DAF67887}.Release|x64.ActiveCfg = Release|Any CPU
{469B4BA9-C2CB-4270-8896-0F04DAF67887}.Release|x64.Build.0 = Release|Any CPU
{469B4BA9-C2CB-4270-8896-0F04DAF67887}.Release|x86.ActiveCfg = Release|Any CPU
{469B4BA9-C2CB-4270-8896-0F04DAF67887}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(SolutionDir)Solution.props" />
<Import Project="$(SolutionDir)Solution.props" Condition="Exists('$(SolutionDir)Solution.props')" />

<PropertyGroup>
<TargetFrameworks>net7.0-windows;net7.0</TargetFrameworks>
Expand Down
2 changes: 1 addition & 1 deletion src/SmartHopper.Components/SmartHopper.Components.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(SolutionDir)Solution.props" />
<Import Project="$(SolutionDir)Solution.props" Condition="Exists('$(SolutionDir)Solution.props')" />

<PropertyGroup>
<TargetFrameworks>net7.0-windows;net7.0</TargetFrameworks>
Expand Down
89 changes: 89 additions & 0 deletions src/SmartHopper.Config.Tests/AIToolManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Xunit;
using SmartHopper.Config.Managers;
using SmartHopper.Config.Models;

namespace SmartHopper.Config.Tests
{
public class AIToolManagerTests
{
private void ResetManager()
{
var managerType = typeof(AIToolManager);
var toolsField = managerType.GetField("_tools", BindingFlags.NonPublic | BindingFlags.Static);
var discoveredField = managerType.GetField("_toolsDiscovered", BindingFlags.NonPublic | BindingFlags.Static);
var toolsDict = (Dictionary<string, AITool>)toolsField.GetValue(null);
toolsDict.Clear();
discoveredField.SetValue(null, false);
}

#if NET7_WINDOWS
[Fact(DisplayName = "RegisterTool_ShouldAddTool [Windows]")]
#else
[Fact(DisplayName = "RegisterTool_ShouldAddTool [Core]")]
#endif
public void RegisterTool_ShouldAddTool()
{
ResetManager();
var tool = new AITool("TestTool", "Test Description", "{}", _ => Task.FromResult((object)"dummy"));
AIToolManager.RegisterTool(tool);
var tools = AIToolManager.GetTools();
Assert.Contains("TestTool", tools.Keys);
Assert.Equal("Test Description", tools["TestTool"].Description);
}

#if NET7_WINDOWS
[Fact(DisplayName = "ExecuteTool_ShouldReturnError_WhenToolNotFound [Windows]")]
#else
[Fact(DisplayName = "ExecuteTool_ShouldReturnError_WhenToolNotFound [Core]")]
#endif
public async Task ExecuteTool_ShouldReturnError_WhenToolNotFound()
{
ResetManager();
var result = await AIToolManager.ExecuteTool("UnknownTool", new JObject(), null);
dynamic dyn = result;
Assert.False(dyn.success);
Assert.Contains("UnknownTool", (string)dyn.error);
}

#if NET7_WINDOWS
[Fact(DisplayName = "ExecuteTool_ShouldExecuteRegisteredTool_WithMergedParameters [Windows]")]
#else
[Fact(DisplayName = "ExecuteTool_ShouldExecuteRegisteredTool_WithMergedParameters [Core]")]
#endif
public async Task ExecuteTool_ShouldExecuteRegisteredTool_WithMergedParameters()
{
ResetManager();
JObject captured = null;
var tool = new AITool("Compute", "Computes value", "{}", p =>
{
captured = p;
int value = p["value"].Value<int>();
return Task.FromResult((object)(value * 2));
});
AIToolManager.RegisterTool(tool);
var parameters = new JObject { ["value"] = 10 };
var extra = new JObject { ["extra"] = 5 };
var result = await AIToolManager.ExecuteTool("Compute", (JObject)parameters.DeepClone(), extra);
Assert.IsType<int>(result);
Assert.Equal(20, (int)result);
Assert.Equal(5, captured["extra"].Value<int>());
}

#if NET7_WINDOWS
[Fact(DisplayName = "GetTools_ShouldBeEmpty_WhenNoToolsRegistered [Windows]")]
#else
[Fact(DisplayName = "GetTools_ShouldBeEmpty_WhenNoToolsRegistered [Core]")]
#endif
public void GetTools_ShouldBeEmpty_WhenNoToolsRegistered()
{
ResetManager();
var tools = AIToolManager.GetTools();
Assert.Empty(tools);
}
}
}
Loading