feat: expose win helper via mcp real lab

This commit is contained in:
zhaoyilun
2026-07-05 14:52:23 +08:00
parent f2dd1c7e57
commit 573a09ade3
23 changed files with 6322 additions and 0 deletions

90
tests/test_draft_queue.py Normal file
View File

@@ -0,0 +1,90 @@
from __future__ import annotations
import json
import tempfile
import unittest
from contextlib import redirect_stdout
from io import StringIO
from pathlib import Path
from isphere_ai_bridge.cli import main
from isphere_ai_bridge.draft_queue import DraftQueue
from isphere_ai_bridge.models import Draft
from isphere_ai_bridge.mock_db import create_mock_msglib
class DraftQueueTests(unittest.TestCase):
def test_append_drafts_writes_pending_queue_and_redacted_audit_event(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
queue_path = Path(tmp) / "drafts.jsonl"
audit_path = Path(tmp) / "audit.jsonl"
queue = DraftQueue(
queue_path=queue_path,
audit_path=audit_path,
clock=lambda: "2026-07-05T01:40:00+08:00",
)
draft = Draft(
message_id="msg-001",
conversation_id="alice@example",
draft_text="Draft only. Please review before sending.",
source_message_ref="MsgLib.db#tblPersonMsg:msg-001",
)
entries = queue.append_drafts([draft])
self.assertEqual(1, len(entries))
self.assertEqual("pending_review", entries[0]["status"])
self.assertEqual("msg-001", entries[0]["message_id"])
self.assertEqual("Draft only. Please review before sending.", entries[0]["draft_text"])
queue_lines = queue_path.read_text(encoding="utf-8").splitlines()
self.assertEqual(1, len(queue_lines))
queue_record = json.loads(queue_lines[0])
self.assertEqual(entries[0]["draft_id"], queue_record["draft_id"])
self.assertEqual("pending_review", queue_record["status"])
audit_lines = audit_path.read_text(encoding="utf-8").splitlines()
self.assertEqual(1, len(audit_lines))
audit_record = json.loads(audit_lines[0])
self.assertEqual("draft_created", audit_record["event_type"])
self.assertEqual(entries[0]["draft_id"], audit_record["draft_id"])
self.assertIn("content_sha256", audit_record)
self.assertNotIn("draft_text", audit_record)
def test_queue_drafts_cli_appends_drafts_and_audit_events(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
db_path = create_mock_msglib(root / "MsgLib.db")
queue_path = root / "drafts.jsonl"
audit_path = root / "audit.jsonl"
stdout = StringIO()
with redirect_stdout(stdout):
exit_code = main(
[
"queue-drafts",
"--db",
str(db_path),
"--queue",
str(queue_path),
"--audit",
str(audit_path),
"--limit",
"2",
]
)
self.assertEqual(0, exit_code)
output = json.loads(stdout.getvalue())
self.assertEqual(2, output["queued"])
self.assertEqual(2, len(queue_path.read_text(encoding="utf-8").splitlines()))
audit_records = [
json.loads(line)
for line in audit_path.read_text(encoding="utf-8").splitlines()
]
self.assertEqual(2, len(audit_records))
self.assertNotIn("draft_text", audit_records[0])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,73 @@
from __future__ import annotations
import sqlite3
import tempfile
import unittest
from pathlib import Path
from isphere_ai_bridge.draft import generate_drafts
from isphere_ai_bridge.local_db import ISphereLocalDbReader, open_readonly_connection
from isphere_ai_bridge.locator import discover_msglib_databases
from isphere_ai_bridge.mock_db import create_mock_msglib
class LocalDbPocTests(unittest.TestCase):
def test_mock_database_reads_normalized_messages(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
db_path = create_mock_msglib(Path(tmp) / "MsgLib.db")
reader = ISphereLocalDbReader(db_path)
probe = reader.probe()
self.assertTrue(probe["can_open_readonly"])
self.assertIn("tblPersonMsg", probe["message_tables"])
self.assertIn("tblMsgGroupPersonMsg", probe["message_tables"])
messages = reader.read_messages(limit=10)
self.assertEqual(4, len(messages))
self.assertEqual("sys-001", messages[0].message_id)
self.assertEqual("system", messages[0].conversation_type)
self.assertEqual("grp-001", messages[1].message_id)
self.assertEqual("group", messages[1].conversation_type)
self.assertEqual("p2p-002", messages[2].message_id)
self.assertEqual("direct", messages[2].conversation_type)
self.assertIn("#tblPersonMsg:p2p-002", messages[2].raw_ref)
def test_connection_is_read_only(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
db_path = create_mock_msglib(Path(tmp) / "MsgLib.db")
with open_readonly_connection(db_path) as con:
with self.assertRaises(sqlite3.OperationalError):
con.execute("create table should_not_write (id int)")
def test_discovers_expected_account_msglib_path(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
base = Path(tmp) / "isphere"
db_path = (
base
/ "connection.10083"
/ "27000000"
/ "Account"
/ "EMP001"
/ "MsgLib.db"
)
create_mock_msglib(db_path)
candidates = discover_msglib_databases(base)
self.assertEqual(1, len(candidates))
self.assertTrue(candidates[0].exists)
self.assertEqual("EMP001", candidates[0].account_id)
self.assertEqual(db_path, candidates[0].path)
def test_generates_draft_only_outputs(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
db_path = create_mock_msglib(Path(tmp) / "MsgLib.db")
messages = ISphereLocalDbReader(db_path).read_messages(limit=2)
drafts = generate_drafts(messages)
self.assertEqual(2, len(drafts))
self.assertEqual("draft-only", drafts[0].status)
self.assertIn("Draft only", drafts[0].draft_text)
if __name__ == "__main__":
unittest.main()

51
tests/test_mcp_api.py Normal file
View File

@@ -0,0 +1,51 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from isphere_ai_bridge import mcp_api
class MCPApiTests(unittest.TestCase):
def test_mcp_api_reads_mock_messages_and_queues_drafts(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
db_path = root / "MsgLib.db"
queue_path = root / "drafts.jsonl"
audit_path = root / "audit.jsonl"
created = mcp_api.create_mock_source(str(db_path))
self.assertEqual(str(db_path), created["created"])
messages = mcp_api.read_messages(str(db_path), limit=2)
self.assertEqual(2, messages["count"])
self.assertEqual("local-db", messages["messages"][0]["source"])
queued = mcp_api.queue_drafts_for_review(
str(db_path),
queue_path=str(queue_path),
audit_path=str(audit_path),
limit=2,
)
self.assertEqual(2, queued["queued"])
self.assertEqual("pending_review", queued["status"])
audit = mcp_api.read_audit_events(str(audit_path), limit=5)
self.assertEqual(2, audit["count"])
self.assertIn("content_sha256", audit["events"][0])
self.assertNotIn("draft_text", audit["events"][0])
def test_limit_is_bounded_for_mcp_tools(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
db_path = Path(tmp) / "MsgLib.db"
mcp_api.create_mock_source(str(db_path))
messages = mcp_api.read_messages(str(db_path), limit=1000)
self.assertEqual(mcp_api.MAX_LIMIT, messages["limit"])
if __name__ == "__main__":
unittest.main()

159
tests/test_native_helper.py Normal file
View File

@@ -0,0 +1,159 @@
from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
from isphere_ai_bridge import native_helper
def _has_framework_csc() -> bool:
windir = Path(os.environ.get("WINDIR", r"C:\Windows"))
return (
windir / "Microsoft.NET" / "Framework64" / "v4.0.30319" / "csc.exe"
).exists() or (
windir / "Microsoft.NET" / "Framework" / "v4.0.30319" / "csc.exe"
).exists()
class NativeHelperTests(unittest.TestCase):
def test_helper_paths_are_actionable(self) -> None:
paths = native_helper.helper_paths()
self.assertIn("build_command", paths)
self.assertTrue(paths["build_script_exists"])
self.assertTrue(paths["default_client_dir_exists"])
@unittest.skipUnless(_has_framework_csc(), ".NET Framework csc.exe is required")
def test_native_helper_build_inspect_and_offline_protocol(self) -> None:
build = native_helper.build_native_helper()
self.assertTrue(build["ok"], build)
self.assertEqual(0, build["returncode"])
self.assertTrue(Path(build["output"]).exists())
self.assertTrue(Path(build["resource_stub"]).exists())
self.assertEqual("x86", build["platform"])
inspect = native_helper.native_lab_inspect()
self.assertTrue(inspect["ok"], inspect)
self.assertTrue(inspect["required_ready"])
type_names = {item["name"] for item in inspect["types"]}
self.assertIn("IMPP.Client.frmMain", type_names)
self.assertIn("IMPP.Client.Business.ChatManager.SingleChat.frmP2PChat", type_names)
bootstrap = native_helper.native_lab_probe_main_window()
self.assertTrue(bootstrap["ok"], bootstrap)
bootstrap_steps = {item["name"]: item["ok"] for item in bootstrap["steps"]}
self.assertTrue(bootstrap_steps["imppmanager_update_user"])
self.assertFalse(bootstrap["main_window_constructed"])
constructed = native_helper.native_lab_probe_main_window(
construct=True,
probe_chat=True,
probe_actions=True,
probe_files=True,
user_jid="mock-bob@offline.lab",
text="native helper direct test",
file_name="incoming-probe-native.txt",
file_size=321,
timeout_ms=15000,
)
self.assertTrue(constructed["ok"], constructed)
self.assertTrue(constructed["main_window_constructed"])
self.assertTrue(constructed["chat_model_constructed"])
self.assertTrue(constructed["chat_window_constructed"])
self.assertTrue(constructed["send_text_invoked"])
self.assertTrue(constructed["receive_text_invoked"])
self.assertTrue(constructed["file_send_control_constructed"])
self.assertTrue(constructed["file_receive_control_constructed"])
construct_steps = {item["name"]: item["ok"] for item in constructed["steps"]}
self.assertTrue(construct_steps["patch_problematic_ui_methods"])
self.assertTrue(construct_steps["instantiate_frmMain"])
self.assertTrue(construct_steps["instantiate_frmP2PChat"])
self.assertTrue(construct_steps["invoke_frmP2PChat_SaveAndShowMessage"])
self.assertTrue(construct_steps["invoke_frmP2PChat_ProccessRecvMessage"])
self.assertTrue(construct_steps["instantiate_UC_FileSend"])
self.assertTrue(construct_steps["instantiate_UC_FileReceive"])
direct_open = native_helper.native_lab_direct_open_main_window(timeout_ms=15000)
self.assertTrue(direct_open["ok"], direct_open)
self.assertTrue(direct_open["main_window_constructed"])
direct_send = native_helper.native_lab_direct_send_message(
"mock-bob@offline.lab",
"direct wrapper send",
timeout_ms=15000,
)
self.assertTrue(direct_send["ok"], direct_send)
self.assertTrue(direct_send["send_text_invoked"])
self.assertFalse(direct_send["receive_text_invoked"])
direct_receive = native_helper.native_lab_direct_receive_message(
"mock-bob@offline.lab",
"direct wrapper receive",
timeout_ms=15000,
)
self.assertTrue(direct_receive["ok"], direct_receive)
self.assertFalse(direct_receive["send_text_invoked"])
self.assertTrue(direct_receive["receive_text_invoked"])
direct_send_file = native_helper.native_lab_direct_send_file(
"mock-bob@offline.lab",
timeout_ms=15000,
)
self.assertTrue(direct_send_file["ok"], direct_send_file)
self.assertTrue(direct_send_file["file_send_control_constructed"])
self.assertFalse(direct_send_file["file_receive_control_constructed"])
direct_receive_file = native_helper.native_lab_direct_receive_file(
"mock-bob@offline.lab",
"direct-wrapper-incoming.txt",
size=222,
timeout_ms=15000,
)
self.assertTrue(direct_receive_file["ok"], direct_receive_file)
self.assertFalse(direct_receive_file["file_send_control_constructed"])
self.assertTrue(direct_receive_file["file_receive_control_constructed"])
with tempfile.TemporaryDirectory() as tmp:
workspace = str(Path(tmp) / "native-lab")
start = native_helper.native_lab_start(workspace=workspace, reset=True)
self.assertTrue(start["started"])
self.assertEqual("offline-lab-native-helper", start["mode"])
self.assertEqual("inspected", start["native_bridge"]["status"])
users = native_helper.native_lab_search_users("文件", workspace=workspace)
self.assertEqual(1, users["count"])
user_jid = users["users"][0]["jid"]
opened = native_helper.native_lab_open_chat(user_jid, workspace=workspace)
self.assertTrue(opened["opened"])
self.assertEqual("p2p-chat", opened["main_window"]["active_panel"])
sent = native_helper.native_lab_send_message(
user_jid,
"native helper test send",
workspace=workspace,
)
self.assertTrue(sent["sent"])
self.assertEqual("frmP2PChat.SaveAndShowMessage", sent["message"]["ui_hook"])
received_file = native_helper.native_lab_receive_file(
user_jid,
"incoming-native.txt",
size=789,
workspace=workspace,
)
self.assertTrue(received_file["received"])
self.assertEqual(
"frmP2PChat.ProccessRecvMessage -> UC_FileReceive",
received_file["file_event"]["ui_hook"],
)
conversation = native_helper.native_lab_read_conversation(user_jid, workspace=workspace)
self.assertEqual(2, conversation["conversation"]["message_count"])
if __name__ == "__main__":
unittest.main()

91
tests/test_offline_lab.py Normal file
View File

@@ -0,0 +1,91 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from isphere_ai_bridge import mcp_api
class OfflineLabTests(unittest.TestCase):
def test_offline_lab_simulates_main_window_search_chat_and_files(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
workspace = str(Path(tmp) / "offline-lab")
start = mcp_api.offline_lab_start(workspace=workspace, reset=True)
self.assertTrue(start["started"])
self.assertEqual("offline-lab-simulated", start["mode"])
self.assertTrue(start["main_window"]["opened"])
users = mcp_api.offline_lab_search_users("文件", workspace=workspace)
self.assertEqual(1, users["count"])
user_jid = users["users"][0]["jid"]
opened = mcp_api.offline_lab_open_chat(user_jid, workspace=workspace)
self.assertTrue(opened["opened"])
self.assertEqual("p2p-chat", opened["main_window"]["active_panel"])
sent = mcp_api.offline_lab_send_message(
user_jid,
"这是一条离线模拟发送消息",
workspace=workspace,
)
self.assertTrue(sent["sent"])
self.assertEqual("outgoing", sent["message"]["direction"])
self.assertEqual("frmP2PChat.SaveAndShowMessage", sent["message"]["ui_hook"])
received = mcp_api.offline_lab_receive_message(
user_jid,
"这是一条离线模拟接收消息",
workspace=workspace,
)
self.assertTrue(received["received"])
self.assertEqual("incoming", received["message"]["direction"])
self.assertEqual("frmP2PChat.ProccessRecvMessage", received["message"]["ui_hook"])
outbound_file = Path(tmp) / "demo.txt"
outbound_file.write_text("file payload", encoding="utf-8")
send_file = mcp_api.offline_lab_send_file(
user_jid,
str(outbound_file),
workspace=workspace,
)
self.assertTrue(send_file["sent"])
self.assertEqual("file", send_file["file_event"]["content_type"])
self.assertEqual(True, send_file["file_event"]["attachments"][0]["exists"])
self.assertEqual(
"frmP2PChat.SendTcpFiles/SendOfflineFiles -> UC_FileSend",
send_file["file_event"]["ui_hook"],
)
receive_file = mcp_api.offline_lab_receive_file(
user_jid,
"incoming-report.pdf",
size=4096,
workspace=workspace,
)
self.assertTrue(receive_file["received"])
self.assertEqual(4096, receive_file["file_event"]["attachments"][0]["size"])
self.assertEqual(
"frmP2PChat.ProccessRecvMessage -> UC_FileReceive",
receive_file["file_event"]["ui_hook"],
)
conversation = mcp_api.offline_lab_read_conversation(user_jid, workspace=workspace)
self.assertEqual(4, conversation["conversation"]["message_count"])
self.assertEqual(4, len(conversation["conversation"]["messages"]))
status = mcp_api.offline_lab_status(workspace=workspace)
self.assertTrue(status["started"])
self.assertEqual(4, status["message_count"])
def test_offline_lab_status_before_start_is_actionable(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
status = mcp_api.offline_lab_status(workspace=str(Path(tmp) / "missing"))
self.assertFalse(status["started"])
self.assertEqual("call offline_lab_start", status["next_step"])
if __name__ == "__main__":
unittest.main()

92
tests/test_real_lab.py Normal file
View File

@@ -0,0 +1,92 @@
from __future__ import annotations
import asyncio
import unittest
from isphere_ai_bridge import real_lab
class RealLoggedInLabTests(unittest.TestCase):
def test_plan_tree_reports_ready_and_blocked_phases(self) -> None:
tree = real_lab.plan_tree()
self.assertEqual("real-loggedin-planning", tree["mode"])
self.assertEqual("A", tree["phases"][0]["id"])
self.assertEqual("ready-now", tree["phases"][0]["status"])
self.assertTrue(
any(phase["status"] == "blocked-waiting-real-data" for phase in tree["phases"])
)
def test_required_files_checklist_has_expected_sections(self) -> None:
checklist = real_lab.required_files_checklist()
section_ids = {section["id"] for section in checklist["sections"]}
self.assertTrue({"install_dir", "user_data", "logged_in_window"}.issubset(section_ids))
self.assertEqual("copy-later", checklist["recommended_timing"])
def test_action_contract_requires_approval_for_send_and_file_upload(self) -> None:
contract = real_lab.action_contract()
actions = {action["name"]: action for action in contract["actions"]}
self.assertFalse(actions["real_lab_search_contacts"]["requires_approval"])
self.assertTrue(actions["real_lab_send_after_approval"]["requires_approval"])
self.assertTrue(actions["real_lab_send_file_after_approval"]["requires_approval"])
self.assertEqual("no-login-automation", contract["hard_limits"][0])
def test_status_and_scan_windows_are_structured_without_real_client(self) -> None:
status = real_lab.status()
scan = real_lab.scan_windows()
self.assertIn("ready", status)
self.assertIn("process_candidates", status)
self.assertIn("next_step", status)
self.assertIn("windows", scan)
self.assertIn("next_step", scan)
def test_win_helper_version_and_self_check_are_structured(self) -> None:
built = real_lab.build_win_helper()
self.assertTrue(built["ok"], built)
version = real_lab.win_helper_version()
self.assertTrue(version["ok"], version)
self.assertEqual("version", version["op"])
self.assertEqual("ISphereWinHelper", version["data"]["helper_name"])
self_check = real_lab.win_helper_self_check()
self.assertTrue(self_check["ok"], self_check)
self.assertEqual("self_check", self_check["op"])
self.assertIn("uia_available", self_check["data"])
def test_win_helper_scan_and_missing_uia_dump_are_structured(self) -> None:
built = real_lab.build_win_helper()
self.assertTrue(built["ok"], built)
scan = real_lab.win_helper_scan_windows(include_all_visible=True)
self.assertTrue(scan["ok"], scan)
self.assertEqual("scan_windows", scan["op"])
self.assertIsInstance(scan["data"]["windows"], list)
dump = real_lab.win_helper_dump_uia("0x0")
self.assertFalse(dump["ok"], dump)
self.assertIn(dump["error"]["code"], {"WINDOW_NOT_FOUND", "UIA_DUMP_FAILED"})
def test_real_lab_tools_are_registered(self) -> None:
from isphere_ai_bridge.server import mcp
names = [tool.name for tool in asyncio.run(mcp.list_tools())]
required = {
"real_lab_plan_tree",
"real_lab_required_files",
"real_lab_action_contract",
"real_lab_status",
"real_lab_scan_windows",
"real_lab_win_helper_version",
"real_lab_win_helper_self_check",
"real_lab_win_helper_scan_windows",
"real_lab_win_helper_dump_uia",
}
self.assertTrue(required.issubset(set(names)))
if __name__ == "__main__":
unittest.main()