feat: validate receive message contract args

This commit is contained in:
zhaoyilun
2026-07-10 01:32:53 +08:00
parent aa636f5a33
commit 95699e05b5
7 changed files with 200 additions and 17 deletions

View File

@@ -26,7 +26,7 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor
- C# `ISphereWinHelper` exists under `native/ISphereWinHelper` and uses the `isphere.helper.v1` stdin/stdout JSON contract. - C# `ISphereWinHelper` exists under `native/ISphereWinHelper` and uses the `isphere.helper.v1` stdin/stdout JSON contract.
- `scripts/verify-win-helper.ps1` and `scripts/verify-go-mcp.ps1` provide repeatable local checks for the helper, eight-tool MCP surface, default empty-source receive/contact/group/file calls, synthetic configured-file receive/contact/group/file smoke, and synthetic configured-directory receive/contact/group/file smoke. - `scripts/verify-win-helper.ps1` and `scripts/verify-go-mcp.ps1` provide repeatable local checks for the helper, eight-tool MCP surface, default empty-source receive/contact/group/file calls, synthetic configured-file receive/contact/group/file smoke, and synthetic configured-directory receive/contact/group/file smoke.
- `isphere_receive_messages` now returns the contract-facing `ok`, `conversation`, `messages`, `next_cursor`, and `audit` envelope while retaining legacy parser-native message fields for compatibility. - `isphere_receive_messages` now returns the contract-facing `ok`, `conversation`, `messages`, `next_cursor`, and `audit` envelope while retaining legacy parser-native message fields for compatibility.
- `isphere_receive_messages` accepts `conversation_id`, `query`, `since`, and `limit` for local readonly PacketReader evidence filtering. - `isphere_receive_messages` accepts the read-contract argument set for the current local-readonly path: `conversation_id`, `query`, `since`, `limit`, `include_attachment_metadata`, `source_preference`, `preview`, and empty `cursor`.
- N12-pre and N12R documents define safe offline evidence intake and internal-sandbox live UIA capture procedures. - N12-pre and N12R documents define safe offline evidence intake and internal-sandbox live UIA capture procedures.
- N13/N14/N15 produced selector catalog/report validation infrastructure, but those are supporting validation assets only. - N13/N14/N15 produced selector catalog/report validation infrastructure, but those are supporting validation assets only.
@@ -53,7 +53,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme
## Current loop ## Current loop
Current loop: `Stage C / Loop C20 - receive-message remaining contract argument validation`. Current loop: `Stage C / Loop C21 - contact/group search remaining contract argument validation`.
Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`. Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`.
@@ -85,10 +85,11 @@ Current loop output so far:
17. C17: `isphere_receive_messages` output was aligned to the core read contract envelope and message fields while retaining legacy aliases; verify-go-mcp now asserts contract fields for configured file and directory source smokes. 17. C17: `isphere_receive_messages` output was aligned to the core read contract envelope and message fields while retaining legacy aliases; verify-go-mcp now asserts contract fields for configured file and directory source smokes.
18. C18: `isphere_receive_messages` gained `conversation_id` and `query` filters across source, MCP args, tests, and configured file/directory smoke verification. 18. C18: `isphere_receive_messages` gained `conversation_id` and `query` filters across source, MCP args, tests, and configured file/directory smoke verification.
19. C19: `isphere_receive_messages` gained RFC3339 `since` filtering against PacketReader epoch-millisecond timestamps plus invalid-since error handling. 19. C19: `isphere_receive_messages` gained RFC3339 `since` filtering against PacketReader epoch-millisecond timestamps plus invalid-since error handling.
20. C20: `isphere_receive_messages` now accepts and validates the remaining safe read-contract args, rejects unsupported source/cursor cases, and can suppress inline attachment metadata.
## Next business mainline ## Next business mainline
1. Run Stage C Loop C20: accept and validate the remaining safe `isphere_receive_messages` contract arguments (`include_attachment_metadata`, `source_preference`, `preview`, and empty `cursor`) without pretending unsupported connectors/pagination exist. 1. Run Stage C Loop C21: apply the same safe-argument validation pattern to `isphere_search_contacts` and `isphere_search_groups`.
2. Leave `isphere_send_message` blocked until bridge/API, UIA action-chain, or protocol connector evidence is validated. 2. Leave `isphere_send_message` blocked until bridge/API, UIA action-chain, or protocol connector evidence is validated.
3. Leave actual file download/cache mapping as a later connector node until a message-to-cache hash or client download source is validated. 3. Leave actual file download/cache mapping as a later connector node until a message-to-cache hash or client download source is validated.
4. Keep UIA selector/report work as fallback support, not as the primary roadmap. 4. Keep UIA selector/report work as fallback support, not as the primary roadmap.

View File

@@ -184,7 +184,7 @@ Allowed parameters:
- `win_helper_self_check`: zero business parameters. - `win_helper_self_check`: zero business parameters.
- `win_helper_scan_windows`: `include_all_visible` only. - `win_helper_scan_windows`: `include_all_visible` only.
- `win_helper_dump_uia`: `hwnd`, `max_depth`, `include_text`, `max_children` only. - `win_helper_dump_uia`: `hwnd`, `max_depth`, `include_text`, `max_children` only.
- `isphere_receive_messages`: `conversation_id`, `query`, `since`, `limit` only. `since` must be RFC3339/RFC3339Nano when supplied. - `isphere_receive_messages`: `conversation_id`, `query`, `since`, `include_attachment_metadata`, `source_preference`, `preview`, `cursor`, `limit` only. `since` must be RFC3339/RFC3339Nano when supplied; `source_preference` currently supports empty/`auto`/`local_readonly`; `cursor` must be empty until pagination is implemented.
- `isphere_search_contacts`: `query`, `limit` only. - `isphere_search_contacts`: `query`, `limit` only.
- `isphere_search_groups`: `query`, `limit` only. - `isphere_search_groups`: `query`, `limit` only.
- `isphere_receive_files`: `conversation_id`, `message_id`, `file_id`, `name_contains`, `mode`, `limit` only. C14 supports `mode` empty or `list`; download is deferred. - `isphere_receive_files`: `conversation_id`, `message_id`, `file_id`, `name_contains`, `mode`, `limit` only. C14 supports `mode` empty or `list`; download is deferred.
@@ -202,7 +202,7 @@ The current runbook verifies eight tools:
- `isphere_search_groups` - `isphere_search_groups`
- `isphere_receive_files` - `isphere_receive_files`
The current log-backed business read tools have parser/source/tool registration and an empty default server source. Raw N12-pre evidence remains under ignored `runs/` paths and is not committed. The current command entry point can read an operator-local encrypted PacketReader log-line file via `ISPHERE_PACKET_LOG_FILE` or a directory of `.log`/`.txt` files via `ISPHERE_PACKET_LOG_DIR`; this is still not a production ingestion claim because raw evidence remains local and uncommitted. `isphere_receive_messages` now exposes contract-facing fields for digital-employee consumption while preserving older aliases for compatibility, and supports `conversation_id`, keyword `query`, and RFC3339 `since` filtering. `isphere_receive_files` currently supports list mode only; download/cache mapping is deferred. The current log-backed business read tools have parser/source/tool registration and an empty default server source. Raw N12-pre evidence remains under ignored `runs/` paths and is not committed. The current command entry point can read an operator-local encrypted PacketReader log-line file via `ISPHERE_PACKET_LOG_FILE` or a directory of `.log`/`.txt` files via `ISPHERE_PACKET_LOG_DIR`; this is still not a production ingestion claim because raw evidence remains local and uncommitted. `isphere_receive_messages` now exposes contract-facing fields for digital-employee consumption while preserving older aliases for compatibility, supports `conversation_id`, keyword `query`, and RFC3339 `since` filtering, and validates the remaining safe contract args without pretending unsupported connectors/pagination exist. `isphere_receive_files` currently supports list mode only; download/cache mapping is deferred.
Core business tools for contacts, groups, messages, and files are defined in `docs/mcp-core-tools-contract.md`. Send capabilities enter the roadmap through that contract after source discovery, connector selection, preview metadata, execution metadata, and audit records are in place. Core business tools for contacts, groups, messages, and files are defined in `docs/mcp-core-tools-contract.md`. Send capabilities enter the roadmap through that contract after source discovery, connector selection, preview metadata, execution metadata, and audit records are in place.

View File

@@ -17,7 +17,7 @@ Schema notes: `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md`
| MCP tool | Primary source | Fallback source | Evidence file | Decision status | Next implementation slice | | MCP tool | Primary source | Fallback source | Evidence file | Decision status | Next implementation slice |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `<message>` stanzas via `internal/isphere.EncryptedPacketLogSource`; configured by `ISPHERE_PACKET_LOG_FILE` or `ISPHERE_PACKET_LOG_DIR` | decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces; wrapped `MsgLib.db` after DB wrapper is solved | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | C19 supports contract-shaped output plus `conversation_id`, `query`, `since`, and `limit` filters; default source remains empty when unset | validate remaining safe contract args | | `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `<message>` stanzas via `internal/isphere.EncryptedPacketLogSource`; configured by `ISPHERE_PACKET_LOG_FILE` or `ISPHERE_PACKET_LOG_DIR` | decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces; wrapped `MsgLib.db` after DB wrapper is solved | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | C20 supports and validates the current safe receive-message contract args; default source remains empty when unset | apply safe contract args to contact/group search |
| `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | decrypted roster/contact stanzas from `Smark.SendReceive`; `MsgLib.db` contact/conversation tables after DB wrapper is solved; UIA helper source if display names are missing | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#log-decryption-result` | C16 verification covers default, configured-file, and configured-directory contact smoke; JID-only display/account fallback | enrich display names from roster/db/uia evidence | | `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | decrypted roster/contact stanzas from `Smark.SendReceive`; `MsgLib.db` contact/conversation tables after DB wrapper is solved; UIA helper source if display names are missing | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#log-decryption-result` | C16 verification covers default, configured-file, and configured-directory contact smoke; JID-only display/account fallback | enrich display names from roster/db/uia evidence |
| `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | `MsgLib.db` group/conversation tables after DB wrapper is solved; UIA helper source if group display names are missing | C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C16 verification covers default, configured-file, and configured-directory group smoke; JID-only display fallback | enrich group display/member data from roster/db/uia evidence | | `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | `MsgLib.db` group/conversation tables after DB wrapper is solved; UIA helper source if group display names are missing | C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C16 verification covers default, configured-file, and configured-directory group smoke; JID-only display fallback | enrich group display/member data from roster/db/uia evidence |
| `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas via `internal/isphere.ListFilesFromMessages` and `isphere_receive_files` list mode; configured PacketReader file or directory source; `zyl\importal\<hash>` cache entries remain unlinked | decrypted `Smark.SendReceive` and `SaveToDB` traces for file-reference reconciliation; future UIA/client connector for download | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#c12-file-transfer-evidence-precheck` | C16 verification covers default, configured-file, and configured-directory file-list smoke; no log-to-cache/download mapping yet | defer download mapping; align read-tool contract output | | `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas via `internal/isphere.ListFilesFromMessages` and `isphere_receive_files` list mode; configured PacketReader file or directory source; `zyl\importal\<hash>` cache entries remain unlinked | decrypted `Smark.SendReceive` and `SaveToDB` traces for file-reference reconciliation; future UIA/client connector for download | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#c12-file-transfer-evidence-precheck` | C16 verification covers default, configured-file, and configured-directory file-list smoke; no log-to-cache/download mapping yet | defer download mapping; align read-tool contract output |

View File

@@ -1693,14 +1693,76 @@ Implemented and verified in this loop:
- Treat `include_attachment_metadata=false` as a request to suppress inline receive-message attachments while leaving `isphere_receive_files` available for explicit file listing. - Treat `include_attachment_metadata=false` as a request to suppress inline receive-message attachments while leaving `isphere_receive_files` available for explicit file listing.
- Do not add write/send/file-download behavior. - Do not add write/send/file-download behavior.
- [ ] **Step 1: Write failing MCP argument validation tests** - [x] **Step 1: Write failing MCP argument validation tests**
Extend `internal/tools/isphere_read_test.go` with accepted safe args and rejected unsupported source/cursor cases. Extend `internal/tools/isphere_read_test.go` with accepted safe args and rejected unsupported source/cursor cases.
- [ ] **Step 2: Implement argument validation and attachment toggle** - [x] **Step 2: Implement argument validation and attachment toggle**
Add minimal validation/helpers in `internal/tools/isphere_read.go`. Add minimal validation/helpers in `internal/tools/isphere_read.go`.
- [x] **Step 3: Update verification and docs**
Update `scripts/verify-go-mcp.ps1`, runbook, status card, and this plan.
- [x] **Step 4: Run full verification and commit**
Run:
```powershell
git diff --check
go test ./...
go build ./cmd/isphere-mcp
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1
```
Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. Commit exact C20 paths only and rewrite the next loop from actual results.
---
## Loop C20 Result
Implemented and verified in this loop:
- `isphere_receive_messages` now accepts the remaining safe read-contract arguments: `include_attachment_metadata`, `source_preference`, `preview`, and `cursor`.
- `source_preference` accepts empty, `auto`, or `local_readonly`; bridge/uia/network are rejected until those connectors exist.
- Empty `cursor` is accepted for contract compatibility; non-empty cursor is rejected until pagination exists.
- `preview` is accepted without mutation; audit execution remains `read`.
- `include_attachment_metadata=false` suppresses inline receive-message attachments while keeping explicit file listing available through `isphere_receive_files`.
- `scripts/verify-go-mcp.ps1` now exercises the accepted contract arguments in default, configured-file, and configured-directory receive-message smokes.
---
## Loop C21: Contact/group search remaining contract argument validation
**Goal:** Apply the same contract-compatibility pattern to `isphere_search_contacts` and `isphere_search_groups` so digital employees can pass safe search-contract fields without tripping schema validation.
**Planned files:**
- Modify: `internal/tools/isphere_contacts.go`
- Modify: `internal/tools/isphere_contacts_test.go`
- Modify: `internal/tools/isphere_groups.go`
- Modify: `internal/tools/isphere_groups_test.go`
- Modify: `scripts/verify-go-mcp.ps1`
- Modify: `docs/go-mcp-runbook.md`
- Modify: `docs/current-status-card.md`
- Modify: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`
**Planned behavior:**
- For contacts: accept `cursor`, `source_preference`, and `include_inactive`.
- For groups: accept `cursor`, `source_preference`, and `include_archived`.
- Accept empty cursor only; reject non-empty cursor until pagination exists.
- Accept source preference empty, `auto`, or `local_readonly`; reject bridge/uia/network until those connectors exist.
- Treat include flags as accepted no-op on local readonly JID-derived candidates because active/archive state is not available in current source.
- Do not add write/send/file-download behavior.
- [ ] **Step 1: Write failing contact/group MCP arg tests**
Extend contact/group tool tests with accepted safe args plus rejected unsupported source/cursor cases.
- [ ] **Step 2: Implement validation helpers**
Add minimal validation to contact and group tool handlers.
- [ ] **Step 3: Update verification and docs** - [ ] **Step 3: Update verification and docs**
Update `scripts/verify-go-mcp.ps1`, runbook, status card, and this plan. Update `scripts/verify-go-mcp.ps1`, runbook, status card, and this plan.
@@ -1716,7 +1778,7 @@ go build ./cmd/isphere-mcp
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1
``` ```
Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. Commit exact C20 paths only and rewrite the next loop from actual results. Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. Commit exact C21 paths only and rewrite the next loop from actual results.
--- ---

View File

@@ -2,6 +2,7 @@ package tools
import ( import (
"context" "context"
"fmt"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -18,10 +19,14 @@ type ReceiveMessagesSource interface {
} }
type ReceiveMessagesArgs struct { type ReceiveMessagesArgs struct {
ConversationID string `json:"conversation_id,omitempty" jsonschema:"exact conversation id filter for local readonly message logs"` ConversationID string `json:"conversation_id,omitempty" jsonschema:"exact conversation id filter for local readonly message logs"`
Query string `json:"query,omitempty" jsonschema:"case-insensitive keyword filter over message text, subject, ids, and participants"` Query string `json:"query,omitempty" jsonschema:"case-insensitive keyword filter over message text, subject, ids, and participants"`
Since string `json:"since,omitempty" jsonschema:"RFC3339 timestamp; return messages at or after this time when packet timestamps are available"` Since string `json:"since,omitempty" jsonschema:"RFC3339 timestamp; return messages at or after this time when packet timestamps are available"`
Limit int `json:"limit,omitempty" jsonschema:"maximum number of messages to return; zero or negative returns all available messages"` Cursor string `json:"cursor,omitempty" jsonschema:"pagination cursor; currently only empty cursor is supported"`
SourcePreference string `json:"source_preference,omitempty" jsonschema:"source selector; currently supports auto or local_readonly only"`
Preview bool `json:"preview,omitempty" jsonschema:"accepted for contract compatibility; read-only execution remains read mode"`
IncludeAttachmentMetadata *bool `json:"include_attachment_metadata,omitempty" jsonschema:"include inline attachment metadata in receive message results; default true"`
Limit int `json:"limit,omitempty" jsonschema:"maximum number of messages to return; zero or negative returns all available messages"`
} }
func RegisterISphereReadTools(server *mcp.Server, source ReceiveMessagesSource) { func RegisterISphereReadTools(server *mcp.Server, source ReceiveMessagesSource) {
@@ -33,6 +38,9 @@ func RegisterISphereReadTools(server *mcp.Server, source ReceiveMessagesSource)
Name: ToolNameReceiveMessages, Name: ToolNameReceiveMessages,
Description: "Return iSphere messages from the configured read-only log-backed source.", Description: "Return iSphere messages from the configured read-only log-backed source.",
}, func(ctx context.Context, _ *mcp.CallToolRequest, input ReceiveMessagesArgs) (*mcp.CallToolResult, map[string]any, error) { }, func(ctx context.Context, _ *mcp.CallToolRequest, input ReceiveMessagesArgs) (*mcp.CallToolResult, map[string]any, error) {
if err := validateReceiveMessagesArgs(input); err != nil {
return nil, nil, err
}
started := time.Now().UTC() started := time.Now().UTC()
result, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{ result, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{
ConversationID: input.ConversationID, ConversationID: input.ConversationID,
@@ -43,14 +51,43 @@ func RegisterISphereReadTools(server *mcp.Server, source ReceiveMessagesSource)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
return nil, receiveMessagesResultToMap(result, started, time.Now().UTC()), nil return nil, receiveMessagesResultToMap(result, started, time.Now().UTC(), receiveMessagesMapOptions{
IncludeAttachmentMetadata: includeReceiveMessageAttachmentMetadata(input),
}), nil
}) })
} }
func receiveMessagesResultToMap(result isphere.ReceiveMessagesResult, started time.Time, finished time.Time) map[string]any { func validateReceiveMessagesArgs(input ReceiveMessagesArgs) error {
sourcePreference := strings.ToLower(strings.TrimSpace(input.SourcePreference))
switch sourcePreference {
case "", "auto", "local_readonly":
default:
return fmt.Errorf("isphere_receive_messages source_preference %q is not available; only auto and local_readonly are supported", input.SourcePreference)
}
if strings.TrimSpace(input.Cursor) != "" {
return fmt.Errorf("isphere_receive_messages cursor pagination is not available yet")
}
return nil
}
func includeReceiveMessageAttachmentMetadata(input ReceiveMessagesArgs) bool {
if input.IncludeAttachmentMetadata == nil {
return true
}
return *input.IncludeAttachmentMetadata
}
type receiveMessagesMapOptions struct {
IncludeAttachmentMetadata bool
}
func receiveMessagesResultToMap(result isphere.ReceiveMessagesResult, started time.Time, finished time.Time, options receiveMessagesMapOptions) map[string]any {
messages := make([]map[string]any, 0, len(result.Messages)) messages := make([]map[string]any, 0, len(result.Messages))
for _, message := range result.Messages { for _, message := range result.Messages {
attachments := receiveMessageAttachments(message) attachments := []map[string]any{}
if options.IncludeAttachmentMetadata {
attachments = receiveMessageAttachments(message)
}
messages = append(messages, map[string]any{ messages = append(messages, map[string]any{
"id": message.ID, "id": message.ID,
"message_id": message.ID, "message_id": message.ID,

View File

@@ -142,6 +142,75 @@ func TestISphereReceiveMessagesToolReturnsSourceMessages(t *testing.T) {
} }
} }
func TestISphereReceiveMessagesToolValidatesRemainingContractArgs(t *testing.T) {
fake := &fakeReceiveMessagesSource{
result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{
ID: "msg-1",
ConversationID: "sender@imopenfire1-lanzhou|receiver@imopenfire1-lanzhou",
ConversationType: "chat",
SenderID: "sender@imopenfire1-lanzhou",
ReceiverID: "receiver@imopenfire1-lanzhou",
Text: "redacted-report.docx",
Timestamp: "1783423807000",
Subject: "FILE_TRANSFER_CANCEL",
}}},
}
session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) {
RegisterISphereReadTools(server, fake)
})
defer cleanup()
callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: ToolNameReceiveMessages,
Arguments: map[string]any{
"include_attachment_metadata": false,
"source_preference": "local_readonly",
"preview": true,
"cursor": "",
"limit": 5,
},
})
if err != nil {
t.Fatalf("call with safe contract args: %v", err)
}
if callResult.IsError {
t.Fatalf("call result is error: %+v", callResult)
}
var decoded struct {
Messages []struct {
Attachments []struct {
FileID string `json:"file_id"`
} `json:"attachments"`
} `json:"messages"`
}
payload, err := json.Marshal(callResult.StructuredContent)
if err != nil {
t.Fatalf("marshal structured content: %v", err)
}
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("decode structured content %s: %v", payload, err)
}
if len(decoded.Messages) != 1 || len(decoded.Messages[0].Attachments) != 0 {
t.Fatalf("attachments = %+v, want suppressed", decoded.Messages)
}
bridgeResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: ToolNameReceiveMessages,
Arguments: map[string]any{"source_preference": "bridge", "limit": 5},
})
if err == nil && !bridgeResult.IsError {
t.Fatalf("bridge source_preference was accepted before connector exists")
}
cursorResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: ToolNameReceiveMessages,
Arguments: map[string]any{"cursor": "next-page", "limit": 5},
})
if err == nil && !cursorResult.IsError {
t.Fatalf("non-empty cursor was accepted before pagination exists")
}
}
type fakeReceiveMessagesSource struct { type fakeReceiveMessagesSource struct {
queries []isphere.ReceiveMessagesQuery queries []isphere.ReceiveMessagesQuery
result isphere.ReceiveMessagesResult result isphere.ReceiveMessagesResult

View File

@@ -172,7 +172,13 @@ func main() {
fail("tools/call win_helper_version", fmt.Errorf("helper_name = %q, want ISphereWinHelper", helperName)) fail("tools/call win_helper_version", fmt.Errorf("helper_name = %q, want ISphereWinHelper", helperName))
} }
receiveResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_receive_messages", Arguments: map[string]any{"limit": 5}}) receiveResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_receive_messages", Arguments: map[string]any{
"source_preference": "local_readonly",
"preview": true,
"cursor": "",
"include_attachment_metadata": true,
"limit": 5,
}})
if err != nil { if err != nil {
fail("tools/call isphere_receive_messages", err) fail("tools/call isphere_receive_messages", err)
} }
@@ -302,6 +308,10 @@ func verifyConfiguredReceiveSource(ctx context.Context) (int, int, int, int) {
"conversation_id": "project-room@conference.imopenfire1-lanzhou|sender@imopenfire1-lanzhou", "conversation_id": "project-room@conference.imopenfire1-lanzhou|sender@imopenfire1-lanzhou",
"query": "report", "query": "report",
"since": "2026-07-07T11:30:00Z", "since": "2026-07-07T11:30:00Z",
"source_preference": "auto",
"preview": true,
"cursor": "",
"include_attachment_metadata": true,
"limit": 5, "limit": 5,
}}) }})
if err != nil { if err != nil {
@@ -427,6 +437,10 @@ func verifyConfiguredDirectorySource(ctx context.Context) (int, int, int, int) {
"conversation_id": "project-room@conference.imopenfire1-lanzhou|sender@imopenfire1-lanzhou", "conversation_id": "project-room@conference.imopenfire1-lanzhou|sender@imopenfire1-lanzhou",
"query": "dir-report", "query": "dir-report",
"since": "2026-07-07T11:30:00Z", "since": "2026-07-07T11:30:00Z",
"source_preference": "auto",
"preview": true,
"cursor": "",
"include_attachment_metadata": true,
"limit": 5, "limit": 5,
}}) }})
if err != nil { if err != nil {