4e3d367637
ci / build (push) Successful in 28s
Инструменты были нарезаны по способу вызова, а не по смыслу: четыре отверстия, двойники *_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 теперь падает, если в публикуемом навыке всплывёт слитое имя.
181 lines
7.8 KiB
C#
181 lines
7.8 KiB
C#
using Kompas.Mcp.Core.Documents;
|
|
using Kompas.Mcp.Core.Modeling;
|
|
using Kompas.Mcp.Core.Query;
|
|
|
|
namespace Kompas.Mcp.Tests.Integration;
|
|
|
|
/// <summary>
|
|
/// Интеграция: механики, на которых держатся слитые инструменты — пакет примитивов эскиза
|
|
/// за один заход, мост «индекс грани → точка на ней», скругление/фаска пачкой рёбер и upsert
|
|
/// переменной. Каждая цепочка проверяется на живом КОМПАС: слияние инструментов не должно
|
|
/// менять результат построения.
|
|
/// </summary>
|
|
[Trait("Category", "Integration")]
|
|
[Collection(KompasCollection.Name)]
|
|
public sealed class MergedToolPathsTests : IntegrationTestBase
|
|
{
|
|
private readonly DocumentService _docs;
|
|
private readonly PartModeler _modeler;
|
|
private readonly QueryService _query;
|
|
private readonly ModelInspectionService _inspection;
|
|
private readonly VariableService _variables;
|
|
|
|
public MergedToolPathsTests(KompasFixture fx) : base(fx)
|
|
{
|
|
_docs = new DocumentService(fx.Session, fx.Dispatcher);
|
|
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
|
|
_query = new QueryService(fx.Session, fx.Dispatcher);
|
|
_inspection = new ModelInspectionService(fx.Session, fx.Dispatcher);
|
|
_variables = new VariableService(fx.Session, fx.Dispatcher);
|
|
}
|
|
|
|
private static SketchEntity Rect(double x1, double y1, double x2, double y2) => new()
|
|
{
|
|
Kind = SketchEntityKind.Rectangle,
|
|
Points = [(x1, y1), (x2, y2)],
|
|
};
|
|
|
|
private static SketchEntity Circle(double cx, double cy, double r) => new()
|
|
{
|
|
Kind = SketchEntityKind.Circle,
|
|
CenterX = cx, CenterY = cy, Radius = r,
|
|
};
|
|
|
|
[Fact]
|
|
public async Task Batch_builds_the_whole_contour_in_one_call()
|
|
{
|
|
await _docs.CreateAsync(KompasDocumentType.Part);
|
|
try
|
|
{
|
|
// Плита 40×20 с четырьмя отверстиями Ø4 — раньше это было 6 вызовов, теперь один.
|
|
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
|
var added = await _modeler.AddEntitiesAsync(s,
|
|
[
|
|
Rect(0, 0, 40, 20),
|
|
Circle(5, 5, 2), Circle(35, 5, 2), Circle(5, 15, 2), Circle(35, 15, 2),
|
|
], autoClose: true);
|
|
Assert.Equal(5, added);
|
|
|
|
await _modeler.ExtrudeAsync(s, depth: 10);
|
|
await _modeler.RebuildAsync();
|
|
|
|
// Объём плиты минус четыре цилиндра.
|
|
var expected = (40 * 20 - 4 * Math.PI * 4) * 10;
|
|
var v = (await _query.GetPartInfoAsync()).Volume;
|
|
Assert.InRange(v, expected * 0.98, expected * 1.02);
|
|
}
|
|
finally { await _docs.CloseAsync(save: false); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Batch_reports_the_failing_position()
|
|
{
|
|
await _docs.CreateAsync(KompasDocumentType.Part);
|
|
try
|
|
{
|
|
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
|
var bad = new SketchEntity { Kind = SketchEntityKind.Circle, CenterX = 0, CenterY = 0, Radius = -1 };
|
|
|
|
var ex = await Assert.ThrowsAnyAsync<Exception>(
|
|
() => _modeler.AddEntitiesAsync(s, [Rect(0, 0, 10, 10), Circle(2, 2, 1), bad]));
|
|
|
|
Assert.Contains("[2]", ex.Message, StringComparison.Ordinal);
|
|
}
|
|
finally { await _docs.CloseAsync(save: false); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Face_center_point_lands_on_the_face()
|
|
{
|
|
await _docs.CreateAsync(KompasDocumentType.Part);
|
|
try
|
|
{
|
|
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
|
await _modeler.AddEntitiesAsync(s, [Rect(0, 0, 40, 20)], autoClose: true);
|
|
await _modeler.ExtrudeAsync(s, depth: 10);
|
|
await _modeler.RebuildAsync();
|
|
|
|
// Мост «индекс → точка»: точка обязана лежать на грани, иначе выбор по ней провалится.
|
|
var faces = await _query.ListFacesAsync();
|
|
Assert.NotEmpty(faces);
|
|
|
|
foreach (var f in faces)
|
|
{
|
|
var (x, y, z) = await _inspection.FaceCenterPointAsync(f.Index);
|
|
Assert.True(double.IsFinite(x) && double.IsFinite(y) && double.IsFinite(z),
|
|
$"грань [{f.Index}]: точка не вычислена");
|
|
|
|
// Точка внутри габарита детали (с допуском на округление).
|
|
Assert.InRange(x, -0.01, 40.01);
|
|
Assert.InRange(y, -0.01, 20.01);
|
|
Assert.InRange(z, -0.01, 10.01);
|
|
}
|
|
|
|
// И по такой точке эскиз на грани действительно создаётся.
|
|
var top = faces.First(f => f.Type == "plane");
|
|
var (px, py, pz) = await _inspection.FaceCenterPointAsync(top.Index);
|
|
var onFace = await _modeler.OpenSketchOnFaceAsync(px, py, pz);
|
|
await _modeler.CloseSketchAsync(onFace);
|
|
}
|
|
finally { await _docs.CloseAsync(save: false); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Fillet_and_chamfer_take_several_edges_at_once()
|
|
{
|
|
await _docs.CreateAsync(KompasDocumentType.Part);
|
|
try
|
|
{
|
|
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
|
await _modeler.AddEntitiesAsync(s, [Rect(0, 0, 40, 20)], autoClose: true);
|
|
await _modeler.ExtrudeAsync(s, depth: 10);
|
|
await _modeler.RebuildAsync();
|
|
|
|
var before = (await _query.GetPartInfoAsync()).Volume;
|
|
var vertical = (await _query.ListEdgesAsync()).Where(e => e.Type == "line").Take(4).Select(e => e.Index).ToList();
|
|
Assert.Equal(4, vertical.Count);
|
|
|
|
// Одна операция на все рёбра: материал убывает, дерево не растёт на четыре узла.
|
|
var featuresBefore = (await _inspection.ListFeaturesAsync()).Count;
|
|
await _modeler.FilletEdgesAsync(vertical, radius: 2);
|
|
await _modeler.RebuildAsync();
|
|
|
|
var after = (await _query.GetPartInfoAsync()).Volume;
|
|
var featuresAfter = (await _inspection.ListFeaturesAsync()).Count;
|
|
|
|
Assert.True(after < before, $"объём не убыл: {before} → {after}");
|
|
Assert.Equal(featuresBefore + 1, featuresAfter);
|
|
}
|
|
finally { await _docs.CloseAsync(save: false); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Set_variable_creates_then_updates()
|
|
{
|
|
await _docs.CreateAsync(KompasDocumentType.Part);
|
|
try
|
|
{
|
|
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
|
await _modeler.AddEntitiesAsync(s, [Rect(0, 0, 40, 20)], autoClose: true);
|
|
await _modeler.ExtrudeAsync(s, depth: 10);
|
|
|
|
var (created, wasCreated) = await _variables.SetOrCreateVariableAsync("width", "40", "ширина плиты");
|
|
Assert.True(wasCreated);
|
|
Assert.Equal(40, created, 3);
|
|
|
|
// Повторный вызов — уже изменение, и формула со ссылкой считается.
|
|
var (updated, wasCreatedAgain) = await _variables.SetOrCreateVariableAsync("width", "50");
|
|
Assert.False(wasCreatedAgain);
|
|
Assert.Equal(50, updated, 3);
|
|
|
|
var (derived, _) = await _variables.SetOrCreateVariableAsync("height", "width/2");
|
|
Assert.Equal(25, derived, 3);
|
|
|
|
var vars = await _inspection.ListVariablesAsync();
|
|
Assert.Contains(vars, v => v.Name == "width" && Math.Abs(v.Value - 50) < 1e-6);
|
|
Assert.Contains(vars, v => v.Name == "height");
|
|
}
|
|
finally { await _docs.CloseAsync(save: false); }
|
|
}
|
|
}
|