Files
isphere-ai-bridge/cmd/isphere-capability-smoke/main.go
2026-07-10 21:22:39 +08:00

674 lines
23 KiB
Go

package main
import (
"context"
"crypto/cipher"
"crypto/des"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
"isphere-ai-bridge/internal/mcpserver"
)
var expectedTools = []string{
"win_helper_version",
"win_helper_self_check",
"win_helper_scan_windows",
"win_helper_dump_uia",
"isphere_receive_messages",
"isphere_search_contacts",
"isphere_search_groups",
"isphere_receive_files",
"isphere_send_file",
"isphere_send_message",
}
type smokeResult struct {
OK bool `json:"ok"`
Package string `json:"package"`
Transport string `json:"transport"`
Initialized bool `json:"initialized"`
ToolCount int `json:"tool_count"`
Tools []string `json:"tools"`
HelperName string `json:"helper_name"`
ReceiveMessageCount int `json:"receive_message_count"`
ContactCount int `json:"contact_count"`
GroupCount int `json:"group_count"`
FileCount int `json:"file_count"`
FixtureReceiveMessageCount int `json:"fixture_receive_message_count"`
FixtureContactCount int `json:"fixture_contact_count"`
FixtureGroupCount int `json:"fixture_group_count"`
FixtureFileCount int `json:"fixture_file_count"`
FixtureDirReceiveMessageCount int `json:"fixture_dir_receive_message_count"`
FixtureDirContactCount int `json:"fixture_dir_contact_count"`
FixtureDirGroupCount int `json:"fixture_dir_group_count"`
FixtureDirFileCount int `json:"fixture_dir_file_count"`
SendPreviewToolPresent bool `json:"send_preview_tool_present"`
ProductionSendEnabled bool `json:"production_send_enabled"`
SendFilePreviewToolPresent bool `json:"send_file_preview_tool_present"`
SendFileProductionEnabled bool `json:"send_file_production_enabled"`
LocalLoginRequired bool `json:"local_login_required"`
RealISphereLoginRequired bool `json:"real_isphere_login_required"`
PythonRuntimeRequired bool `json:"python_runtime_required"`
NoRealSend bool `json:"no_real_send"`
ActionToolsPresent bool `json:"action_tools_present"`
PacketLogFileConfiguredAfterRun bool `json:"packet_log_file_configured_after_run"`
PacketLogDirConfiguredAfterRun bool `json:"packet_log_dir_configured_after_run"`
MsgLibConfigured bool `json:"msglib_configured"`
SendAuditPath string `json:"send_audit_path"`
SendIdempotencyPath string `json:"send_idempotency_path"`
}
func main() {
if err := run(); err != nil {
fail("capability_smoke", err)
}
}
func run() error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
clearRuntimeEnv()
sendStateDir := filepath.Join("runs", "capability-smoke", fmt.Sprintf("state-%d", time.Now().UnixNano()))
if err := os.MkdirAll(sendStateDir, 0o755); err != nil {
return err
}
auditPath := filepath.Join(sendStateDir, "send-preview.jsonl")
if err := os.Setenv("ISPHERE_SEND_AUDIT_PATH", auditPath); err != nil {
return err
}
idempotencyPath := filepath.Join(sendStateDir, "send-idempotency.jsonl")
if err := os.Setenv("ISPHERE_SEND_IDEMPOTENCY_PATH", idempotencyPath); err != nil {
return err
}
sendFileDir := filepath.Join(sendStateDir, "send-file-allowed")
if err := os.MkdirAll(sendFileDir, 0o755); err != nil {
return err
}
sendFilePath := filepath.Join(sendFileDir, "redacted-preview.txt")
if err := os.WriteFile(sendFilePath, []byte("hello"), 0o600); err != nil {
return err
}
if err := os.Setenv("ISPHERE_SEND_FILE_ALLOWED_DIR", sendFileDir); err != nil {
return err
}
session, cleanup, err := newSession(ctx, "isphere-capability-smoke")
if err != nil {
return err
}
defer cleanup()
toolNames, err := listToolNames(ctx, session)
if err != nil {
return err
}
if err := requireExpectedTools(toolNames); err != nil {
return err
}
helperName, err := verifyHelperVersion(ctx, session)
if err != nil {
return err
}
receiveCount, err := verifyReceiveMessages(ctx, session, 0)
if err != nil {
return err
}
contactCount, err := verifySearchContacts(ctx, session, "sender", 0)
if err != nil {
return err
}
groupCount, err := verifySearchGroups(ctx, session, "project", 0)
if err != nil {
return err
}
fileCount, err := verifyReceiveFiles(ctx, session, "report", 0, "", "")
if err != nil {
return err
}
sendPreviewOK, productionSendEnabled, err := verifySendMessage(ctx, session)
if err != nil {
return err
}
sendFilePreviewOK, sendFileProductionEnabled, err := verifySendFile(ctx, session, sendFilePath)
if err != nil {
return err
}
fixtureReceiveCount, fixtureContactCount, fixtureGroupCount, fixtureFileCount, err := verifyPacketLogFileFixture(ctx)
if err != nil {
return err
}
fixtureDirReceiveCount, fixtureDirContactCount, fixtureDirGroupCount, fixtureDirFileCount, err := verifyPacketLogDirFixture(ctx)
if err != nil {
return err
}
result := smokeResult{
OK: true,
Package: "send-capability-test",
Transport: "sdk_in_memory",
Initialized: true,
ToolCount: len(toolNames),
Tools: toolNames,
HelperName: helperName,
ReceiveMessageCount: receiveCount,
ContactCount: contactCount,
GroupCount: groupCount,
FileCount: fileCount,
FixtureReceiveMessageCount: fixtureReceiveCount,
FixtureContactCount: fixtureContactCount,
FixtureGroupCount: fixtureGroupCount,
FixtureFileCount: fixtureFileCount,
FixtureDirReceiveMessageCount: fixtureDirReceiveCount,
FixtureDirContactCount: fixtureDirContactCount,
FixtureDirGroupCount: fixtureDirGroupCount,
FixtureDirFileCount: fixtureDirFileCount,
SendPreviewToolPresent: sendPreviewOK,
ProductionSendEnabled: productionSendEnabled,
SendFilePreviewToolPresent: sendFilePreviewOK,
SendFileProductionEnabled: sendFileProductionEnabled,
LocalLoginRequired: false,
RealISphereLoginRequired: false,
PythonRuntimeRequired: false,
NoRealSend: true,
ActionToolsPresent: false,
PacketLogFileConfiguredAfterRun: os.Getenv("ISPHERE_PACKET_LOG_FILE") != "",
PacketLogDirConfiguredAfterRun: os.Getenv("ISPHERE_PACKET_LOG_DIR") != "",
MsgLibConfigured: false,
SendAuditPath: auditPath,
SendIdempotencyPath: idempotencyPath,
}
encoded, err := json.Marshal(result)
if err != nil {
return err
}
fmt.Println(string(encoded))
return nil
}
func clearRuntimeEnv() {
for _, key := range []string{
"ISPHERE_PACKET_LOG_FILE",
"ISPHERE_PACKET_LOG_DIR",
"ISPHERE_MSGLIB_SIDECAR_EXE",
"ISPHERE_MSGLIB_SQLITE_DLL",
"ISPHERE_MSGLIB_DB",
"ISPHERE_MSGLIB_PASSWORD",
"ISPHERE_SEND_FILE_ALLOWED_DIR",
} {
_ = os.Unsetenv(key)
}
}
func newSession(ctx context.Context, name string) (*mcp.ClientSession, func(), error) {
serverTransport, clientTransport := mcp.NewInMemoryTransports()
server, err := mcpserver.NewServerFromEnv()
if err != nil {
return nil, nil, fmt.Errorf("server config: %w", err)
}
serverCtx, cancel := context.WithCancel(ctx)
serverErr := make(chan error, 1)
go func() {
serverErr <- server.Run(serverCtx, serverTransport)
}()
client := mcp.NewClient(&mcp.Implementation{Name: name, Version: "0.1.0"}, nil)
session, err := client.Connect(serverCtx, clientTransport, nil)
if err != nil {
cancel()
return nil, nil, fmt.Errorf("connect: %w", err)
}
cleanup := func() {
_ = session.Close()
cancel()
select {
case <-serverErr:
case <-time.After(2 * time.Second):
fmt.Fprintln(os.Stderr, `{"ok":false,"step":"server_shutdown","error":"server did not stop before timeout"}`)
}
}
return session, cleanup, nil
}
func listToolNames(ctx context.Context, session *mcp.ClientSession) ([]string, error) {
result, err := session.ListTools(ctx, &mcp.ListToolsParams{})
if err != nil {
return nil, fmt.Errorf("list tools: %w", err)
}
names := make([]string, 0, len(result.Tools))
for _, tool := range result.Tools {
names = append(names, tool.Name)
}
sort.Strings(names)
return names, nil
}
func requireExpectedTools(got []string) error {
want := append([]string(nil), expectedTools...)
sort.Strings(want)
if !reflect.DeepEqual(got, want) {
return fmt.Errorf("tool names = %#v, want %#v", got, want)
}
return nil
}
func verifyHelperVersion(ctx context.Context, session *mcp.ClientSession) (string, error) {
payload, err := callToolObject(ctx, session, "win_helper_version", map[string]any{})
if err != nil {
return "", err
}
if payload["ok"] != true {
return "", fmt.Errorf("win_helper_version ok = %#v, want true", payload["ok"])
}
data, ok := payload["data"].(map[string]any)
if !ok {
return "", fmt.Errorf("win_helper_version data missing: %#v", payload["data"])
}
helperName, _ := data["helper_name"].(string)
if helperName != "ISphereWinHelper" {
return "", fmt.Errorf("helper_name = %q, want ISphereWinHelper", helperName)
}
return helperName, nil
}
func verifyReceiveMessages(ctx context.Context, session *mcp.ClientSession, wantCount int) (int, error) {
payload, err := callToolObject(ctx, session, "isphere_receive_messages", map[string]any{
"source_preference": "local_readonly",
"preview": true,
"cursor": "",
"include_attachment_metadata": true,
"limit": 5,
})
if err != nil {
return 0, err
}
if payload["ok"] != true {
return 0, fmt.Errorf("isphere_receive_messages ok = %#v, want true", payload["ok"])
}
messages, ok := payload["messages"].([]any)
if !ok {
return 0, fmt.Errorf("messages missing or not array: %#v", payload["messages"])
}
if len(messages) != wantCount {
return 0, fmt.Errorf("message count = %d, want %d", len(messages), wantCount)
}
return len(messages), nil
}
func verifySearchContacts(ctx context.Context, session *mcp.ClientSession, query string, wantCount int) (int, error) {
payload, err := callToolObject(ctx, session, "isphere_search_contacts", map[string]any{
"query": query,
"source_preference": "local_readonly",
"include_inactive": true,
"cursor": "",
"limit": 5,
})
if err != nil {
return 0, err
}
if payload["ok"] != true {
return 0, fmt.Errorf("isphere_search_contacts ok = %#v, want true", payload["ok"])
}
contacts, ok := payload["contacts"].([]any)
if !ok {
return 0, fmt.Errorf("contacts missing or not array: %#v", payload["contacts"])
}
if len(contacts) != wantCount {
return 0, fmt.Errorf("contact count = %d, want %d", len(contacts), wantCount)
}
return len(contacts), nil
}
func verifySearchGroups(ctx context.Context, session *mcp.ClientSession, query string, wantCount int) (int, error) {
payload, err := callToolObject(ctx, session, "isphere_search_groups", map[string]any{
"query": query,
"source_preference": "local_readonly",
"include_archived": true,
"cursor": "",
"limit": 5,
})
if err != nil {
return 0, err
}
if payload["ok"] != true {
return 0, fmt.Errorf("isphere_search_groups ok = %#v, want true", payload["ok"])
}
groups, ok := payload["groups"].([]any)
if !ok {
return 0, fmt.Errorf("groups missing or not array: %#v", payload["groups"])
}
if len(groups) != wantCount {
return 0, fmt.Errorf("group count = %d, want %d", len(groups), wantCount)
}
return len(groups), nil
}
func verifyReceiveFiles(ctx context.Context, session *mcp.ClientSession, nameContains string, wantCount int, wantFileID string, wantFileName string) (int, error) {
payload, err := callToolObject(ctx, session, "isphere_receive_files", map[string]any{
"mode": "list",
"name_contains": nameContains,
"source_preference": "local_readonly",
"preview": true,
"cursor": "",
"output_dir": "",
"limit": 5,
})
if err != nil {
return 0, err
}
if payload["ok"] != true {
return 0, fmt.Errorf("isphere_receive_files ok = %#v, want true", payload["ok"])
}
files, ok := payload["files"].([]any)
if !ok {
return 0, fmt.Errorf("files missing or not array: %#v", payload["files"])
}
if len(files) != wantCount {
return 0, fmt.Errorf("file count = %d, want %d", len(files), wantCount)
}
if wantCount > 0 {
first, ok := files[0].(map[string]any)
if !ok {
return 0, fmt.Errorf("first file not object: %#v", files[0])
}
if first["file_id"] != wantFileID || first["file_name"] != wantFileName {
return 0, fmt.Errorf("unexpected file: %#v", first)
}
if first["download_ref"] != nil || first["saved_path"] != nil || first["sha256"] != nil {
return 0, fmt.Errorf("file list should not download or save: %#v", first)
}
}
return len(files), nil
}
func verifySendMessage(ctx context.Context, session *mcp.ClientSession) (bool, bool, error) {
content := "redacted preview body"
contentHash := sha256Hex(content)
preview, err := callToolObject(ctx, session, "isphere_send_message", map[string]any{
"target_type": "direct",
"target_id": "sender@imopenfire1-lanzhou",
"content_text": content,
"content_sha256": contentHash,
"idempotency_key": "capability-preview-idempotency-key",
"execution_mode": "preview",
})
if err != nil {
return false, false, err
}
if preview["ok"] != true || preview["send_status"] != "planned" || preview["connector_stage"] != "preview" {
return false, false, fmt.Errorf("unexpected send preview payload: %#v", preview)
}
sideEffects, ok := preview["side_effects"].(map[string]any)
if !ok || sideEffects["sent_message"] != false || sideEffects["clicked_ui"] != false || sideEffects["typed_text"] != false {
return false, false, fmt.Errorf("send preview side effects mismatch: %#v", preview["side_effects"])
}
production, err := callToolObject(ctx, session, "isphere_send_message", map[string]any{
"target_type": "direct",
"target_id": "sender@imopenfire1-lanzhou",
"content_text": content,
"content_sha256": contentHash,
"idempotency_key": "capability-production-idempotency-key",
"execution_mode": "production",
})
if err != nil {
return false, false, err
}
if production["ok"] != false || production["send_status"] != "blocked" || production["connector_stage"] != "blocked" {
return false, false, fmt.Errorf("production send was not blocked: %#v", production)
}
productionSideEffects, ok := production["side_effects"].(map[string]any)
if !ok || productionSideEffects["sent_message"] != false || productionSideEffects["uploaded_file"] != false || productionSideEffects["captured_network"] != false {
return false, false, fmt.Errorf("production side effects mismatch: %#v", production["side_effects"])
}
return true, false, nil
}
func verifySendFile(ctx context.Context, session *mcp.ClientSession, filePath string) (bool, bool, error) {
fileHash, err := sha256FileHex(filePath)
if err != nil {
return false, false, err
}
preview, err := callToolObject(ctx, session, "isphere_send_file", map[string]any{
"target_type": "direct",
"target_id": "sender@imopenfire1-lanzhou",
"file_path": filePath,
"file_sha256": fileHash,
"idempotency_key": "capability-file-preview-idempotency-key",
"execution_mode": "preview",
"preview": true,
})
if err != nil {
return false, false, err
}
if preview["ok"] != true || preview["send_status"] != "planned" || preview["file_sha256"] != fileHash || preview["file_size_bytes"] != float64(5) {
return false, false, fmt.Errorf("unexpected send-file preview payload: %#v", preview)
}
sideEffects, ok := preview["side_effect_flags"].(map[string]any)
if !ok || sideEffects["sent_file"] != false || sideEffects["uploaded_file"] != false || sideEffects["clicked_ui"] != false {
return false, false, fmt.Errorf("send-file preview side effects mismatch: %#v", preview["side_effect_flags"])
}
if preview["production_send_enabled"] != false || preview["real_send_attempted"] != false {
return false, false, fmt.Errorf("send-file preview should not enable or attempt send: %#v", preview)
}
production, err := callToolObject(ctx, session, "isphere_send_file", map[string]any{
"target_type": "direct",
"target_id": "sender@imopenfire1-lanzhou",
"file_path": filePath,
"file_sha256": fileHash,
"idempotency_key": "capability-file-production-idempotency-key",
"execution_mode": "production",
})
if err != nil {
return false, false, err
}
if production["ok"] != false || production["send_status"] != "blocked" || production["production_send_enabled"] != false || production["real_send_attempted"] != false {
return false, false, fmt.Errorf("production send-file was not blocked: %#v", production)
}
productionSideEffects, ok := production["side_effect_flags"].(map[string]any)
if !ok || productionSideEffects["sent_file"] != false || productionSideEffects["uploaded_file"] != false || productionSideEffects["captured_network"] != false {
return false, false, fmt.Errorf("production send-file side effects mismatch: %#v", production["side_effect_flags"])
}
return true, false, nil
}
func verifyPacketLogFileFixture(ctx context.Context) (int, int, int, int, error) {
plaintext := fixturePacketLogPlaintext("msg-fixture-1", "redacted-report.docx")
line, err := encryptPacketLogLine(plaintext)
if err != nil {
return 0, 0, 0, 0, err
}
file, err := os.CreateTemp("", "isphere-capability-packet-*.log")
if err != nil {
return 0, 0, 0, 0, err
}
fixturePath := file.Name()
defer os.Remove(fixturePath)
if _, err := file.WriteString("\n" + line + "\n"); err != nil {
_ = file.Close()
return 0, 0, 0, 0, err
}
if err := file.Close(); err != nil {
return 0, 0, 0, 0, err
}
restore := replaceEnv("ISPHERE_PACKET_LOG_FILE", fixturePath)
defer restore()
_ = os.Unsetenv("ISPHERE_PACKET_LOG_DIR")
session, cleanup, err := newSession(ctx, "isphere-capability-smoke-file-fixture")
if err != nil {
return 0, 0, 0, 0, err
}
defer cleanup()
receiveCount, err := verifyReceiveMessages(ctx, session, 1)
if err != nil {
return 0, 0, 0, 0, err
}
contactCount, err := verifySearchContacts(ctx, session, "sender", 1)
if err != nil {
return 0, 0, 0, 0, err
}
groupCount, err := verifySearchGroups(ctx, session, "project", 1)
if err != nil {
return 0, 0, 0, 0, err
}
fileCount, err := verifyReceiveFiles(ctx, session, "report", 1, "msg-fixture-1:redacted-report.docx", "redacted-report.docx")
if err != nil {
return 0, 0, 0, 0, err
}
return receiveCount, contactCount, groupCount, fileCount, nil
}
func verifyPacketLogDirFixture(ctx context.Context) (int, int, int, int, error) {
plaintext := fixturePacketLogPlaintext("msg-dir-fixture-1", "redacted-dir-report.docx")
line, err := encryptPacketLogLine(plaintext)
if err != nil {
return 0, 0, 0, 0, err
}
dir, err := os.MkdirTemp("", "isphere-capability-packet-dir-*")
if err != nil {
return 0, 0, 0, 0, err
}
defer os.RemoveAll(dir)
if err := os.WriteFile(filepath.Join(dir, "a-packet-reader.log"), []byte("\n"+line+"\n"), 0o600); err != nil {
return 0, 0, 0, 0, err
}
if err := os.WriteFile(filepath.Join(dir, "ignored.tmp"), []byte("ignored\n"), 0o600); err != nil {
return 0, 0, 0, 0, err
}
restoreFile := replaceEnv("ISPHERE_PACKET_LOG_FILE", "")
restoreDir := replaceEnv("ISPHERE_PACKET_LOG_DIR", dir)
defer restoreFile()
defer restoreDir()
session, cleanup, err := newSession(ctx, "isphere-capability-smoke-dir-fixture")
if err != nil {
return 0, 0, 0, 0, err
}
defer cleanup()
receiveCount, err := verifyReceiveMessages(ctx, session, 1)
if err != nil {
return 0, 0, 0, 0, err
}
contactCount, err := verifySearchContacts(ctx, session, "sender", 1)
if err != nil {
return 0, 0, 0, 0, err
}
groupCount, err := verifySearchGroups(ctx, session, "project", 1)
if err != nil {
return 0, 0, 0, 0, err
}
fileCount, err := verifyReceiveFiles(ctx, session, "dir-report", 1, "msg-dir-fixture-1:redacted-dir-report.docx", "redacted-dir-report.docx")
if err != nil {
return 0, 0, 0, 0, err
}
return receiveCount, contactCount, groupCount, fileCount, nil
}
func callToolObject(ctx context.Context, session *mcp.ClientSession, name string, args map[string]any) (map[string]any, error) {
result, err := session.CallTool(ctx, &mcp.CallToolParams{Name: name, Arguments: args})
if err != nil {
return nil, fmt.Errorf("%s: %w", name, err)
}
if result.IsError {
return nil, fmt.Errorf("%s returned IsError=true: %#v", name, result.Content)
}
payload, err := json.Marshal(result.StructuredContent)
if err != nil {
return nil, fmt.Errorf("%s marshal structuredContent: %w", name, err)
}
var object map[string]any
if err := json.Unmarshal(payload, &object); err != nil {
return nil, fmt.Errorf("%s decode structuredContent %s: %w", name, payload, err)
}
return object, nil
}
func fixturePacketLogPlaintext(messageID string, body string) string {
return "--------------------------------------------------------------------------------------------------------------------------------------------\n" +
"2026/7/7 15:30:07\n" +
"<message id=\"" + messageID + "\" from=\"project-room@conference.imopenfire1-lanzhou/imp_pc_4.1.2.6842\" to=\"sender@imopenfire1-lanzhou\" type=\"groupchat\">\n" +
" <body>" + body + "</body>\n" +
" <received xmlns=\"urn:xmpp:receipts\" id=\"receipt-" + messageID + "\" type=\"1\" stamp=\"\" />\n" +
" <subject>FILE_TRANSFER_CANCEL</subject>\n" +
" <isphere xmlns=\"isphere.im\" type=\"1001\" sendtime=\"1783423807000\" version=\"1\" />\n" +
" <readed />\n" +
"</message>"
}
func encryptPacketLogLine(plaintext string) (string, error) {
block, err := des.NewCipher([]byte("hyhccdtm"))
if err != nil {
return "", err
}
plain := padPKCS7([]byte(plaintext), des.BlockSize)
ciphertext := make([]byte, len(plain))
iv := []byte{0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF}
cipher.NewCBCEncrypter(block, iv).CryptBlocks(ciphertext, plain)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func padPKCS7(data []byte, blockSize int) []byte {
pad := blockSize - len(data)%blockSize
out := append([]byte(nil), data...)
for i := 0; i < pad; i++ {
out = append(out, byte(pad))
}
return out
}
func sha256Hex(value string) string {
sum := sha256.Sum256([]byte(value))
return hex.EncodeToString(sum[:])
}
func sha256FileHex(path string) (string, error) {
payload, err := os.ReadFile(path)
if err != nil {
return "", err
}
return sha256Hex(string(payload)), nil
}
func replaceEnv(key string, value string) func() {
oldValue, hadOldValue := os.LookupEnv(key)
if value == "" {
_ = os.Unsetenv(key)
} else {
_ = os.Setenv(key, value)
}
return func() {
if hadOldValue {
_ = os.Setenv(key, oldValue)
} else {
_ = os.Unsetenv(key)
}
}
}
func fail(step string, err error) {
encoded, _ := json.Marshal(map[string]any{
"ok": false,
"step": step,
"error": err.Error(),
})
fmt.Fprintln(os.Stderr, string(encoded))
os.Exit(1)
}