feat: add send file preview tool
This commit is contained in:
@@ -11,6 +11,7 @@ $expectedTools = @(
|
||||
"isphere_search_contacts",
|
||||
"isphere_search_groups",
|
||||
"isphere_receive_files",
|
||||
"isphere_send_file",
|
||||
"isphere_send_message"
|
||||
)
|
||||
|
||||
@@ -89,6 +90,7 @@ var expectedTools = []string{
|
||||
"isphere_search_contacts",
|
||||
"isphere_search_groups",
|
||||
"isphere_receive_files",
|
||||
"isphere_send_file",
|
||||
"isphere_send_message",
|
||||
}
|
||||
|
||||
@@ -98,7 +100,7 @@ func main() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
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"} {
|
||||
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"} {
|
||||
if err := os.Unsetenv(key); err != nil {
|
||||
fail("server/env", err)
|
||||
}
|
||||
@@ -114,6 +116,18 @@ func main() {
|
||||
if err := os.Setenv("ISPHERE_SEND_IDEMPOTENCY_PATH", filepath.Join(sendStateDir, "send-idempotency.jsonl")); err != nil {
|
||||
fail("server/send_state", err)
|
||||
}
|
||||
sendFileDir, err := os.MkdirTemp("", "isphere-send-file-*")
|
||||
if err != nil {
|
||||
fail("server/send_file_state", err)
|
||||
}
|
||||
defer os.RemoveAll(sendFileDir)
|
||||
sendFilePath := filepath.Join(sendFileDir, "redacted-preview.txt")
|
||||
if err := os.WriteFile(sendFilePath, []byte("hello"), 0o600); err != nil {
|
||||
fail("server/send_file_state", err)
|
||||
}
|
||||
if err := os.Setenv("ISPHERE_SEND_FILE_ALLOWED_DIR", sendFileDir); err != nil {
|
||||
fail("server/send_file_state", err)
|
||||
}
|
||||
|
||||
serverTransport, clientTransport := mcp.NewInMemoryTransports()
|
||||
server, err := mcpserver.NewServerFromEnv()
|
||||
@@ -158,7 +172,7 @@ func main() {
|
||||
sort.Strings(toolNames)
|
||||
want := append([]string(nil), expectedTools...)
|
||||
sort.Strings(want)
|
||||
if len(toolNames) != 9 || !reflect.DeepEqual(toolNames, want) {
|
||||
if len(toolNames) != 10 || !reflect.DeepEqual(toolNames, want) {
|
||||
fail("tools/list", fmt.Errorf("tool names = %#v, want %#v", toolNames, want))
|
||||
}
|
||||
|
||||
@@ -221,6 +235,7 @@ func main() {
|
||||
defaultGroupCount := verifySearchGroups(ctx, session, "project", "tools/call isphere_search_groups default", 0)
|
||||
defaultFileCount := verifyReceiveFiles(ctx, session, "report", "tools/call isphere_receive_files default", 0, "", "")
|
||||
sendPreviewOK, productionSendEnabled := verifySendMessagePreview(ctx, session)
|
||||
sendFilePreviewOK, sendFileProductionEnabled := verifySendFilePreview(ctx, session, sendFilePath)
|
||||
configuredReceiveCount, configuredContactCount, configuredGroupCount, configuredFileCount := verifyConfiguredReceiveSource(ctx)
|
||||
configuredDirReceiveCount, configuredDirContactCount, configuredDirGroupCount, configuredDirFileCount := verifyConfiguredDirectorySource(ctx)
|
||||
|
||||
@@ -250,6 +265,8 @@ func main() {
|
||||
"action_tools_present": false,
|
||||
"send_preview_tool_present": sendPreviewOK,
|
||||
"production_send_enabled": productionSendEnabled,
|
||||
"send_file_preview_tool_present": sendFilePreviewOK,
|
||||
"send_file_production_enabled": sendFileProductionEnabled,
|
||||
"msglib_configured": false,
|
||||
}
|
||||
encoded, _ := json.Marshal(result)
|
||||
@@ -569,6 +586,68 @@ func verifySendMessagePreview(ctx context.Context, session *mcp.ClientSession) (
|
||||
return true, false
|
||||
}
|
||||
|
||||
func verifySendFilePreview(ctx context.Context, session *mcp.ClientSession, filePath string) (bool, bool) {
|
||||
fileHash := sha256FileHexForHarness(filePath)
|
||||
callResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_send_file", Arguments: map[string]any{
|
||||
"target_type": "direct",
|
||||
"target_id": "sender@imopenfire1-lanzhou",
|
||||
"file_path": filePath,
|
||||
"file_sha256": fileHash,
|
||||
"idempotency_key": "verify-file-preview-idempotency-key",
|
||||
"execution_mode": "preview",
|
||||
"preview": true,
|
||||
}})
|
||||
if err != nil {
|
||||
fail("tools/call isphere_send_file preview", err)
|
||||
}
|
||||
if callResult.IsError {
|
||||
fail("tools/call isphere_send_file preview", fmt.Errorf("tool returned isError=true: %#v", callResult.Content))
|
||||
}
|
||||
var previewPayload map[string]any
|
||||
encoded, _ := json.Marshal(callResult.StructuredContent)
|
||||
if err := json.Unmarshal(encoded, &previewPayload); err != nil {
|
||||
fail("tools/call isphere_send_file preview", fmt.Errorf("decode structuredContent %s: %w", encoded, err))
|
||||
}
|
||||
if previewPayload["ok"] != true || previewPayload["send_status"] != "planned" || previewPayload["file_sha256"] != fileHash || previewPayload["file_size_bytes"] != float64(5) {
|
||||
fail("tools/call isphere_send_file preview", fmt.Errorf("unexpected send-file preview status: %s", encoded))
|
||||
}
|
||||
if previewPayload["production_send_enabled"] != false || previewPayload["real_send_attempted"] != false {
|
||||
fail("tools/call isphere_send_file preview", fmt.Errorf("send-file preview should not enable or attempt send: %s", encoded))
|
||||
}
|
||||
sideEffects, ok := previewPayload["side_effect_flags"].(map[string]any)
|
||||
if !ok || sideEffects["sent_file"] != false || sideEffects["uploaded_file"] != false || sideEffects["clicked_ui"] != false {
|
||||
fail("tools/call isphere_send_file preview", fmt.Errorf("unexpected send-file side effects: %s", encoded))
|
||||
}
|
||||
|
||||
productionResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_send_file", Arguments: map[string]any{
|
||||
"target_type": "direct",
|
||||
"target_id": "sender@imopenfire1-lanzhou",
|
||||
"file_path": filePath,
|
||||
"file_sha256": fileHash,
|
||||
"idempotency_key": "verify-file-production-idempotency-key",
|
||||
"execution_mode": "production",
|
||||
}})
|
||||
if err != nil {
|
||||
fail("tools/call isphere_send_file production_blocked", err)
|
||||
}
|
||||
if productionResult.IsError {
|
||||
fail("tools/call isphere_send_file production_blocked", fmt.Errorf("tool returned isError=true: %#v", productionResult.Content))
|
||||
}
|
||||
var productionPayload map[string]any
|
||||
productionEncoded, _ := json.Marshal(productionResult.StructuredContent)
|
||||
if err := json.Unmarshal(productionEncoded, &productionPayload); err != nil {
|
||||
fail("tools/call isphere_send_file production_blocked", fmt.Errorf("decode structuredContent %s: %w", productionEncoded, err))
|
||||
}
|
||||
if productionPayload["ok"] != false || productionPayload["send_status"] != "blocked" || productionPayload["production_send_enabled"] != false || productionPayload["real_send_attempted"] != false {
|
||||
fail("tools/call isphere_send_file production_blocked", fmt.Errorf("production file send was not blocked: %s", productionEncoded))
|
||||
}
|
||||
productionSideEffects, ok := productionPayload["side_effect_flags"].(map[string]any)
|
||||
if !ok || productionSideEffects["sent_file"] != false || productionSideEffects["uploaded_file"] != false || productionSideEffects["captured_network"] != false {
|
||||
fail("tools/call isphere_send_file production_blocked", fmt.Errorf("unexpected production file side effects: %s", productionEncoded))
|
||||
}
|
||||
return true, false
|
||||
}
|
||||
|
||||
func verifySearchContacts(ctx context.Context, session *mcp.ClientSession, query string, step string, wantCount int) int {
|
||||
callResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_search_contacts", Arguments: map[string]any{
|
||||
"query": query,
|
||||
@@ -716,6 +795,14 @@ func sha256HexForHarness(value string) string {
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func sha256FileHexForHarness(path string) string {
|
||||
payload, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
fail("fixture/file_hash", err)
|
||||
}
|
||||
return sha256HexForHarness(string(payload))
|
||||
}
|
||||
|
||||
func padPKCS7ForHarness(data []byte, blockSize int) []byte {
|
||||
pad := blockSize - len(data)%blockSize
|
||||
out := append([]byte(nil), data...)
|
||||
@@ -752,7 +839,7 @@ func fail(step string, err error) {
|
||||
|
||||
Assert-True ($script:harnessJson.ok -eq $true) "SDK harness did not return ok=true"
|
||||
Assert-True ($script:harnessJson.helper_name -eq "ISphereWinHelper") "SDK harness helper_name mismatch: $($script:harnessJson.helper_name)"
|
||||
Assert-True ($script:harnessJson.tool_count -eq 9) "SDK harness tool_count mismatch: $($script:harnessJson.tool_count)"
|
||||
Assert-True ($script:harnessJson.tool_count -eq 10) "SDK harness tool_count mismatch: $($script:harnessJson.tool_count)"
|
||||
Assert-True ($script:harnessJson.receive_message_count -eq 0) "SDK harness receive_message_count mismatch: $($script:harnessJson.receive_message_count)"
|
||||
Assert-True ($script:harnessJson.contact_count -eq 0) "SDK harness contact_count mismatch: $($script:harnessJson.contact_count)"
|
||||
Assert-True ($script:harnessJson.group_count -eq 0) "SDK harness group_count mismatch: $($script:harnessJson.group_count)"
|
||||
@@ -767,6 +854,8 @@ func fail(step string, err error) {
|
||||
Assert-True ($script:harnessJson.configured_dir_file_count -eq 1) "SDK harness configured_dir_file_count mismatch: $($script:harnessJson.configured_dir_file_count)"
|
||||
Assert-True ($script:harnessJson.send_preview_tool_present -eq $true) "SDK harness send_preview_tool_present mismatch: $($script:harnessJson.send_preview_tool_present)"
|
||||
Assert-True ($script:harnessJson.production_send_enabled -eq $false) "SDK harness production_send_enabled mismatch: $($script:harnessJson.production_send_enabled)"
|
||||
Assert-True ($script:harnessJson.send_file_preview_tool_present -eq $true) "SDK harness send_file_preview_tool_present mismatch: $($script:harnessJson.send_file_preview_tool_present)"
|
||||
Assert-True ($script:harnessJson.send_file_production_enabled -eq $false) "SDK harness send_file_production_enabled mismatch: $($script:harnessJson.send_file_production_enabled)"
|
||||
|
||||
[ordered]@{
|
||||
ok = $true
|
||||
@@ -789,6 +878,8 @@ func fail(step string, err error) {
|
||||
configured_dir_file_count = $script:harnessJson.configured_dir_file_count
|
||||
send_preview_tool_present = $script:harnessJson.send_preview_tool_present
|
||||
production_send_enabled = $script:harnessJson.production_send_enabled
|
||||
send_file_preview_tool_present = $script:harnessJson.send_file_preview_tool_present
|
||||
send_file_production_enabled = $script:harnessJson.send_file_production_enabled
|
||||
packet_log_file_configured = $script:harnessJson.packet_log_file_configured
|
||||
packet_log_dir_configured = $script:harnessJson.packet_log_dir_configured
|
||||
python_runtime_required = $false
|
||||
|
||||
Reference in New Issue
Block a user