Files
kompas3d-mcp/tests/Kompas.Mcp.Tests/PluginSkillsTests.cs
T
mikhail 4e3d367637
ci / build (push) Successful in 28s
Каталог инструментов сжат под агента: 84 → 56
Инструменты были нарезаны по способу вызова, а не по смыслу: четыре отверстия,
двойники *_index, одиннадцать sketch_add_*. Агент платил за это дважды — 40 КБ
описаний в каждой сессии и лишние round-trip'ы, а каждый вызов это ещё и шанс
сбиться. Теперь инструмент называет ОПЕРАЦИЮ, вариант задаётся параметром,
объект выбирается индексом или точкой одним и тем же инструментом.

Проверка построения приходит сама. Каждая мутирующая операция дописывает к ответу
итог validate_part (AutoValidation): Create()/Update()==true не значит успех, а
правило навыка «проверяй после каждого шага» удваивало число вызовов. Выключается
через set_auto_validate или KOMPAS_MCP_AUTOVALIDATE=0 — сбой самой проверки уходит
в примечание и никогда не превращает удачную операцию в ошибку.

Эскиз: 16 инструментов → 3. sketch_create(plane|faceIndex|x,y,z, entities[],
autoClose) строит контур целиком; пакет выполняется за ОДИН заход на STA-поток
(PartModeler.AddEntitiesAsync), ошибка называет позицию примитива в списке.

Слияния: hole(type=simple|counterbore|countersink|conic), extrude/revolve(mode),
pattern(kind), mirror (без featureIds — всё тело), document_save(path?),
set_variable как upsert (разведочный вызов «есть ли такая» больше не нужен),
list_faces/list_edges(index?) вместо отдельных describe_*, get_part_info и
get_bounding_box — в describe_model(sections), где незапрошенные разделы вообще
не читаются из модели.

Селектор index|point: fillet_edge/chamfer_edge принимают edgeIndices списком —
одна операция дерева на все рёбра; для операций API7, умеющих только точку,
индекс переводится в точку через ModelInspectionService.FaceCenterPointAsync
(середина параметрической области грани).

Схема слитого инструмента не запрещает неверную комбинацию полей — это делает
валидация, и её сообщение называет type и недостающий параметр.

Тесты: 291 unit (+41) и 136 integration (+5), интеграционные — на живом КОМПАС.
Новые проверяют ровно рискованные места: точка-из-индекса лежит на грани и по ней
создаётся эскиз, мульти-ребёрное скругление даёт один узел дерева и убыль объёма,
upsert создаёт и затем меняет переменную с формулой, пакет сообщает позицию сбоя.
PluginSkillsTests теперь падает, если в публикуемом навыке всплывёт слитое имя.
2026-07-31 11:52:30 +03:00

75 lines
3.0 KiB
C#

namespace Kompas.Mcp.Tests;
/// <summary>Публикуемые навыки должны быть самодостаточны: без ссылок на внутренности репозитория.</summary>
[Trait("Category", "Unit")]
public sealed class PluginSkillsTests
{
private static string SkillsRoot => Path.Combine(TestPaths.RepoRoot(), "plugin", "skills");
public static TheoryData<string> SkillFiles()
{
var data = new TheoryData<string>();
foreach (var file in Directory.GetFiles(SkillsRoot, "*.md", SearchOption.AllDirectories))
data.Add(file);
return data;
}
[Theory]
[MemberData(nameof(SkillFiles))]
public void Skill_has_no_repository_only_references(string file)
{
var text = File.ReadAllText(file);
string[] forbidden = ["usecases/", "src/Kompas", "kompas-sdk-research", "docs-maintainer", "docs/Kompas3D_SDK"];
foreach (var token in forbidden)
Assert.DoesNotContain(token, text, StringComparison.Ordinal);
}
[Theory]
[MemberData(nameof(SkillFiles))]
public void Skill_has_no_stale_tool_count(string file)
{
var text = File.ReadAllText(file);
string[] staleCounts = ["46 инструментов", "83 инструмента", "84 инструмента"];
foreach (var stale in staleCounts)
Assert.DoesNotContain(stale, text, StringComparison.Ordinal);
}
[Theory]
[MemberData(nameof(SkillFiles))]
public void Skill_does_not_reference_merged_away_tools(string file)
{
var text = File.ReadAllText(file);
// Инструменты, слитые в общие: навык, зовущий их по имени, стоит агенту потерянного хода.
string[] gone =
[
"sketch_add_", "sketch_create_on_face", "sketch_create_on_offset_plane",
"extrude_boss", "extrude_cut", "revolve_boss", "revolve_cut",
"fillet_edge_index", "chamfer_edge_index",
"hole_counterbore", "hole_countersink", "hole_conic",
"linear_pattern", "circular_pattern", "mirror_operation", "mirror_body",
"document_save_as", "create_variable", "set_variable_note",
"get_part_info", "get_bounding_box", "describe_face", "describe_edge",
];
foreach (var name in gone)
Assert.DoesNotContain(name, text, StringComparison.Ordinal);
}
[Fact]
public void Both_skills_are_present_with_frontmatter()
{
foreach (var name in new[] { "kompas-3d", "kompas-fdm-design" })
{
var path = Path.Combine(SkillsRoot, name, "SKILL.md");
Assert.True(File.Exists(path), $"нет навыка {path}");
var text = File.ReadAllText(path);
Assert.StartsWith("---", text, StringComparison.Ordinal);
Assert.Contains($"name: {name}", text, StringComparison.Ordinal);
Assert.Contains("description:", text, StringComparison.Ordinal);
}
}
}