feat: add emulated tool-calling bridge for Lingma

Add a proxy-side tool emulation layer so Lingma requests can surface stable OpenAI tool_calls and Anthropic tool_use blocks even when upstream tool events are missing or inconsistent.

Constraint: Keep native Lingma tool event bridging as the first path and layer emulation as a fallback

Rejected: Depend exclusively on Lingma native tool/invoke events | tool visibility remains inconsistent across models and transports

Confidence: high

Scope-risk: moderate
This commit is contained in:
mmc
2026-05-07 18:10:01 +08:00
parent 5911e4322e
commit 94a8025ae5
11 changed files with 1808 additions and 4 deletions

View File

@@ -3,10 +3,12 @@ from __future__ import annotations
import json
import os
import sys
import tempfile
import types
import unittest
from types import SimpleNamespace
from unittest.mock import patch
import zipfile
# app.lingma_pool imports auto_login; tests here don't execute Playwright paths.
# Stub module import so test environments without playwright can import pool code.
@@ -28,6 +30,7 @@ sys.modules.setdefault("playwright", _playwright)
sys.modules.setdefault("playwright.async_api", _playwright_async)
from app.config import _parse_accounts, load_settings
from app.bootstrap_lingma import bootstrap_from_vsix
from app.lingma_pool import LingmaPool
from app.stats import StatsCollector, estimate_tokens
@@ -212,5 +215,57 @@ class ConfigParsingTests(unittest.TestCase):
self.assertEqual(settings.tool_allowlist, [])
class BootstrapLingmaTests(unittest.TestCase):
def _make_test_vsix(self, root: str) -> str:
nested_zip_path = os.path.join(root, "nested.zip")
with zipfile.ZipFile(nested_zip_path, "w") as nested:
nested.writestr("2.5.20/x86_64_linux/Lingma", b"new-binary")
nested.writestr("2.5.20/extension/main.js", b"console.log('ok')")
vsix_path = os.path.join(root, "test.vsix")
with zipfile.ZipFile(vsix_path, "w") as vsix:
with open(nested_zip_path, "rb") as nested_file:
vsix.writestr(
"extension/dist/bin/lingma-2.5.20.zip",
nested_file.read(),
)
return vsix_path
def test_bootstrap_refreshes_when_extension_assets_missing(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
bin_dir = os.path.join(tmpdir, "data", "bin")
release_dir = os.path.join(bin_dir, "2.5.20")
os.makedirs(release_dir, exist_ok=True)
lingma_bin = os.path.join(bin_dir, "Lingma")
with open(lingma_bin, "wb") as f:
f.write(b"old-binary")
marker = {
"version": "2.5.20",
"release_root": "2.5.20",
}
with open(os.path.join(bin_dir, ".lingma-bootstrap.json"), "w", encoding="utf-8") as f:
json.dump(marker, f)
vsix_path = self._make_test_vsix(tmpdir)
env = {
"LINGMA_BIN": lingma_bin,
"LINGMA_SOURCE_TYPE": "vsix",
"LINGMA_VSIX_URL": f"file://{vsix_path}",
"LINGMA_BOOTSTRAP_ALWAYS": "false",
"LINGMA_FORCE_REFRESH": "false",
}
with patch.dict(os.environ, env, clear=False):
bootstrap_from_vsix()
with open(lingma_bin, "rb") as f:
self.assertEqual(f.read(), b"new-binary")
self.assertTrue(
os.path.exists(os.path.join(release_dir, "extension", "main.js"))
)
if __name__ == "__main__":
unittest.main()