fix(server): stop stacking a tool profile's visibility on every session
Release / test (full, windows-latest, 3.11) (push) Canceled after 0s
Release / test (pdf, ubuntu-latest, 3.11) (push) Canceled after 0s
Release / test (pdf, ubuntu-latest, 3.12) (push) Canceled after 0s
Release / build (push) Canceled after 0s
Release / publish-pypi (push) Canceled after 0s
Release / github-release (push) Canceled after 0s

What the first Linux CI run of the 1.6 tree found.

- fastmcp 3's enable()/disable() append a Visibility transform per call,
  and a tool lookup walks the transform stack as nested calls. The
  lifespan re-applies the tool profile on every client connection, so an
  in-process server grew two layers per session until every tool call,
  system_status included, raised RecursionError (CPython 3.11 counts C
  frames against the limit). The profile's transforms now replace the
  previous ones, as _apply_discovery_mode already did; a test switches
  the profile 600 times and then calls a tool. The local suite runs in
  half the time it took with the stack growing.
- 21 fake-ActiveX tests build a real pywin32 VARIANT; they now skip
  without pywin32, like the suite's other VARIANT-dependent COM tests,
  and the Windows lane keeps running them.
- A test spelled a path through a directory it never created, which only
  Windows normalises away; it creates the directory.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-25 00:49:16 +03:00
co-authored by Claude Opus 5.5
parent 17bf3a39ce
commit cdb1063896
8 changed files with 99 additions and 1 deletions
+23
View File
@@ -9,12 +9,20 @@ is dispatched at all).
from __future__ import annotations
import importlib.util
import types
import pytest
from backends.base import EntityInfo
#: A path that marshals a point or an object array builds a real pywin32 VARIANT,
#: and pywin32 exists only on Windows. The Windows lane runs these; elsewhere
#: they skip, like the suite's other VARIANT-dependent COM tests.
needs_pywin32 = pytest.mark.skipif(
importlib.util.find_spec("win32com") is None, reason="pywin32 not installed"
)
pytestmark = pytest.mark.asyncio
@@ -251,6 +259,7 @@ async def test_a_non_string_block_name_is_a_type_error_on_both_placing_tools(bac
await backend.block_insert("", 0, 0)
@needs_pywin32
async def test_com_block_ref_probes_the_block_table_before_insert_block(com):
backend, document, space = com
with pytest.raises(ValueError, match="'NO_SUCH_BLOCK' is not defined"):
@@ -332,6 +341,7 @@ async def test_explode_refuses_a_non_insert_without_writing(backend):
assert len(list(backend._msp())) == before
@needs_pywin32
async def test_com_explode_deletes_attdefs_adds_text_per_attrib_and_deletes_the_reference(com):
backend, document, space = com
attdef = _FakeObject("AcDbAttributeDefinition", "D1", Constant=False)
@@ -395,6 +405,7 @@ async def test_com_explode_refuses_a_non_insert_before_any_call(com):
assert space.calls == []
@needs_pywin32
async def test_com_explode_adds_the_text_to_the_owner_block_not_the_active_layout(com):
"""ActiveX ``Explode()`` places the members in the owner space; the tag
TEXT must land beside them, not in whatever tab is active."""
@@ -456,6 +467,7 @@ async def test_com_explode_refuses_a_nested_reference_before_explode(com):
assert dispatched == [] and space.calls == [] and ref.deleted is False
@needs_pywin32
async def test_com_explode_carries_the_attrib_frame_and_style_onto_the_text(com):
"""The COM twin of the mirrored-reference test. ``AddText`` builds a +Z
TEXT; an ATTRIB reflected to ``Normal (0, 0, -1)`` has an OCS ``Rotation``
@@ -541,6 +553,7 @@ async def test_com_explode_carries_the_attrib_frame_and_style_onto_the_text(com)
}
@needs_pywin32
async def test_com_explode_skips_frame_members_the_attrib_does_not_expose(com):
"""A bare ATTRIB (the shape the earlier fakes use) still explodes: an
absent optional member is skipped, never written as a guess, and a
@@ -580,6 +593,7 @@ async def test_com_explode_skips_frame_members_the_attrib_does_not_expose(com):
assert names == ["Rotation", "Alignment", "Layer"]
@needs_pywin32
async def test_com_explode_turns_a_constant_attdef_into_text_instead_of_deleting_it(com):
"""``GetAttributes()`` excludes constant attributes (``GetConstantAttributes()``
lists them) and ``Explode()`` returns each as an ``AcDbAttributeDefinition``
@@ -645,6 +659,7 @@ async def test_com_explode_turns_a_constant_attdef_into_text_instead_of_deleting
], "the constant ATTDEF's placement, angle and alignment ride onto its TEXT"
@needs_pywin32
async def test_com_explode_falls_back_to_the_constant_tag_list_when_constant_is_absent(com):
"""An object that does not expose ``Constant`` is still recognised through
``GetConstantAttributes()``; a value-less placeholder is still just deleted."""
@@ -937,6 +952,7 @@ async def test_explode_of_a_single_cell_grid_is_still_an_ordinary_insert(backend
assert [(await backend.entity_get(h)).type for h in result["inserted_handles"]] == ["CIRCLE"]
@needs_pywin32
async def test_com_explode_turns_a_multi_line_attrib_into_mtext(com):
"""``TextString`` of a multi-line attribute is one flattened string; the
content is ``MTextAttributeContent`` and the box ``MTextBoundaryWidth``.
@@ -1018,6 +1034,7 @@ async def test_com_explode_turns_a_multi_line_attrib_into_mtext(com):
assert ref.deleted is True
@needs_pywin32
async def test_com_explode_multi_line_attrib_takes_the_frame_before_the_angle(com):
backend, document, space = com
note = _FakeObject(
@@ -1689,6 +1706,7 @@ class _SpaceFailingSecondAddText(_FakeSpace):
return super().AddText(text, point, height)
@needs_pywin32
async def test_com_explode_undoes_a_burst_that_fails_after_explode(com):
"""ActiveX ``Explode()`` leaves the reference in place, so a failure after
it (here ``AddText`` on the second attribute; a constant ATTDEF whose
@@ -1794,6 +1812,7 @@ async def test_set_attributes_refuses_by_tag_and_leaves_the_value_alone(backend)
assert await backend.block_get_attributes(ref.handle) == {"TAG": "7"}
@needs_pywin32
async def test_com_insert_refuses_before_insert_block_and_writes_numbers_as_text(com):
backend, document, space = com
space.attributes = {"TAG": ""}
@@ -1876,6 +1895,7 @@ async def test_define_on_an_existing_layer_reports_nothing_created(backend):
assert result["layers_created"] == [], "layer 0 always exists"
@needs_pywin32
async def test_com_define_probes_layers_before_blocks_add(com):
backend, document, space = com
with pytest.raises(ValueError, match="'NOT_A_LAYER' do not exist"):
@@ -1921,6 +1941,7 @@ async def test_define_treats_an_existing_layer_case_insensitively(backend):
assert result["layers_created"] == []
@needs_pywin32
async def test_com_define_creates_a_case_folded_layer_once(com):
backend, document, space = com
with pytest.raises(ValueError, match=r"layer\(s\) 'Alpha' do not exist"):
@@ -1933,6 +1954,7 @@ async def test_com_define_creates_a_case_folded_layer_once(com):
assert [name for name, _ in document.Blocks.blocks["PID_CASE"].calls] == ["AddLine", "AddLine"]
@needs_pywin32
async def test_com_define_treats_an_existing_layer_case_insensitively(com):
backend, document, space = com
document.Layers.Add("PID-EQUIP")
@@ -1941,6 +1963,7 @@ async def test_com_define_treats_an_existing_layer_case_insensitively(com):
assert result["layers_created"] == [] and document.Layers.added == ["PID-EQUIP"]
@needs_pywin32
async def test_com_define_refuses_a_bad_base_point_before_blocks_add(com):
backend, document, space = com
with pytest.raises(TypeError, match="'base_y'"):