test: add receive display msglib smoke
This commit is contained in:
@@ -56,7 +56,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme
|
|||||||
|
|
||||||
## Current loop
|
## Current loop
|
||||||
|
|
||||||
Current loop: `Stage C / Loop C33 - optional real receive-message display smoke`.
|
Current loop: `Stage C / Loop C34 - MsgLib enrichment operator runbook and setup smoke`.
|
||||||
|
|
||||||
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`.
|
||||||
|
|
||||||
@@ -101,10 +101,11 @@ Current loop output so far:
|
|||||||
30. C30: wired `DisplayEntities` into `isphere_search_contacts` and `isphere_search_groups` as an optional env-configured enrichment source with fake-source tests; default MCP verification now clears MsgLib env to keep the standard smoke deterministic.
|
30. C30: wired `DisplayEntities` into `isphere_search_contacts` and `isphere_search_groups` as an optional env-configured enrichment source with fake-source tests; default MCP verification now clears MsgLib env to keep the standard smoke deterministic.
|
||||||
31. C31: added optional `scripts/verify-msglib-mcp-enrichment.ps1`; real copied-DB MCP smoke returned sanitized contact/group counts and MsgLib source refs without printing names, JIDs, message bodies, file paths, or raw rows.
|
31. C31: added optional `scripts/verify-msglib-mcp-enrichment.ps1`; real copied-DB MCP smoke returned sanitized contact/group counts and MsgLib source refs without printing names, JIDs, message bodies, file paths, or raw rows.
|
||||||
32. C32: `isphere_receive_messages` can now use the optional MsgLib display-entity source to fill safe `sender_name` and `conversation.display_name` fields while preserving message text/content/attachment behavior.
|
32. C32: `isphere_receive_messages` can now use the optional MsgLib display-entity source to fill safe `sender_name` and `conversation.display_name` fields while preserving message text/content/attachment behavior.
|
||||||
|
33. C33: optional real copied-DB MCP smoke now also constructs a synthetic encrypted PacketReader fixture from internally selected MsgLib contact/group metadata and verifies receive-message display enrichment through MCP; output stays sanitized to counts, booleans, and `msglib:<source_table>` refs only.
|
||||||
|
|
||||||
## Next business mainline
|
## Next business mainline
|
||||||
|
|
||||||
1. Run Stage C Loop C33: add an optional real copied-DB smoke for receive-message display-name enrichment, keeping output sanitized by default.
|
1. Run Stage C Loop C34: document and verify the operator setup path for enabling optional MsgLib enrichment in MCP without exposing local names, JIDs, file paths, message bodies, or raw rows.
|
||||||
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.
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ Committed C29 implementation:
|
|||||||
- Go wrapper: `DisplayEntities(ctx, DisplayEntitiesOptions)`.
|
- Go wrapper: `DisplayEntities(ctx, DisplayEntitiesOptions)`.
|
||||||
- Verification script calls `display_entities` only when copied DB env paths are supplied, and prints only sanitized `entity_count` plus `source_tables` summaries.
|
- Verification script calls `display_entities` only when copied DB env paths are supplied, and prints only sanitized `entity_count` plus `source_tables` summaries.
|
||||||
- Evidence-backed C29 verification against the copied DB returned contact/group entity counts and source-table names without printing entity values.
|
- Evidence-backed C29 verification against the copied DB returned contact/group entity counts and source-table names without printing entity values.
|
||||||
- C31 adds optional `scripts/verify-msglib-mcp-enrichment.ps1` to prove MCP-level contact/group enrichment through the real sidecar while printing only counts and `msglib:<source_table>` refs.
|
- C31 adds optional `scripts/verify-msglib-mcp-enrichment.ps1` to prove MCP-level contact/group enrichment through the real sidecar while printing only counts and `msglib:<source_table>` refs. C33 extends the same optional script to create a synthetic encrypted PacketReader fixture from internally selected MsgLib contact/group metadata and verify receive-message `sender_name` plus `conversation.display_name` enrichment; the printed result remains limited to counts, booleans, and source refs.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
@@ -201,3 +201,13 @@ $env:ISPHERE_MSGLIB_SQLITE_DLL = "<path-to-System.Data.SQLite.dll>"
|
|||||||
$env:ISPHERE_MSGLIB_DB = "<path-to-copied-MsgLib.db>"
|
$env:ISPHERE_MSGLIB_DB = "<path-to-copied-MsgLib.db>"
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-msglib-sidecar.ps1
|
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-msglib-sidecar.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Optional MCP enrichment smoke, including receive-message display enrichment, is also opt-in through the same copied DB environment variables:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:ISPHERE_MSGLIB_SQLITE_DLL = "<path-to-System.Data.SQLite.dll>"
|
||||||
|
$env:ISPHERE_MSGLIB_DB = "<path-to-copied-MsgLib.db>"
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-msglib-mcp-enrichment.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected printed evidence is sanitized: contact/group counts, `msglib:<source_table>` refs, `receive_display_smoke=true`, populated-field booleans, and no entity values, message bodies, file paths, or raw rows.
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ 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; copied `MsgLib.db` through a future x86 read-only sidecar/helper | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence`; `docs/source-discovery/2026-07-10-dotnet-wrapper-static-analysis.md` | C20 supports and validates the current safe receive-message contract args; C32 wires optional MsgLib display metadata into receive-message `sender_name` and `conversation.display_name`; DB-backed message listing is still not implemented | C33 optional real receive-message display smoke |
|
| `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; copied `MsgLib.db` through a future x86 read-only sidecar/helper | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence`; `docs/source-discovery/2026-07-10-dotnet-wrapper-static-analysis.md` | C20 supports and validates the current safe receive-message contract args; C32 wires optional MsgLib display metadata into receive-message `sender_name` and `conversation.display_name`; C33 proves this path through a real copied-DB MCP smoke with sanitized output; DB-backed message listing is still not implemented | C34 operator runbook/setup smoke |
|
||||||
| `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `TD_Roster`, `TD_CustomEffigy`, `tblRecent`, `tblChatLevel`, `tblPersonMsg`; decrypted roster/contact stanzas from `Smark.SendReceive`; UIA helper source if display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md` | C32 reuses contact display metadata for receive-message `sender_name`; search enrichment remains available | C33 optional real receive-message display smoke |
|
| `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `TD_Roster`, `TD_CustomEffigy`, `tblRecent`, `tblChatLevel`, `tblPersonMsg`; decrypted roster/contact stanzas from `Smark.SendReceive`; UIA helper source if display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md` | C33 proves real copied-DB contact display metadata can flow into receive-message `sender_name` through MCP while output remains sanitized; search enrichment remains available | C34 operator runbook/setup smoke |
|
||||||
| `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `tblMsgGroupPersonMsg`, `TD_WorkGroupAuth`, `tblRecent`; UIA helper source if group display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md`; C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C32 reuses group display metadata for receive-message `conversation.display_name`; search enrichment remains available | C33 optional real receive-message display smoke |
|
| `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `tblMsgGroupPersonMsg`, `TD_WorkGroupAuth`, `tblRecent`; UIA helper source if group display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md`; C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C33 proves real copied-DB group display metadata can flow into receive-message `conversation.display_name` through MCP while output remains sanitized; search enrichment remains available | C34 operator runbook/setup smoke |
|
||||||
| `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` | C22 verifies safe file-list contract args; no log-to-cache/download mapping yet | defer download mapping; precheck contact/group display-name 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` | C22 verifies safe file-list contract args; no log-to-cache/download mapping yet | defer download mapping; precheck contact/group display-name evidence |
|
||||||
| `isphere_send_message` | none validated yet | existing bridge/API/local service preferred; UIA write connector only after internal-sandbox action evidence; network/protocol connector only after protocol discovery | `docs/source-discovery/2026-07-10-send-message-source-precheck.md` | C15 blocked: contract exists, but no committed bridge/API/local service, helper write op, UIA action chain, or protocol connector is validated | do not implement write behavior; harden existing read source configuration |
|
| `isphere_send_message` | none validated yet | existing bridge/API/local service preferred; UIA write connector only after internal-sandbox action evidence; network/protocol connector only after protocol discovery | `docs/source-discovery/2026-07-10-send-message-source-precheck.md` | C15 blocked: contract exists, but no committed bridge/API/local service, helper write op, UIA action chain, or protocol connector is validated | do not implement write behavior; harden existing read source configuration |
|
||||||
| `isphere_send_file` | none validated yet | depends on send-message connector plus outbound file/upload evidence | `docs/source-discovery/2026-07-10-send-message-source-precheck.md` | blocked behind `isphere_send_message` connector selection and file upload/source policy | defer until send-message connector is validated |
|
| `isphere_send_file` | none validated yet | depends on send-message connector plus outbound file/upload evidence | `docs/source-discovery/2026-07-10-send-message-source-precheck.md` | blocked behind `isphere_send_message` connector selection and file upload/source policy | defer until send-message connector is validated |
|
||||||
@@ -39,4 +39,4 @@ Start Stage C with a narrow log-backed `isphere_receive_messages` source abstrac
|
|||||||
|
|
||||||
## Deferred source work
|
## Deferred source work
|
||||||
|
|
||||||
`MsgLib.db` has a validated copied read-only open path through the bundled 32-bit `System.Data.SQLite.dll` and password `123`. C27 adds `MsgLibReadSidecar` as the bounded x86 .NET reader boundary, C28 adds the Go `internal/msglib` process client, C29 adds bounded `display_entities` extraction, C30 wires it as optional contact/group MCP enrichment, C31 proves real copied-DB MCP enrichment with sanitized output, and C32 reuses it for receive-message display fields. The next step is an optional real receive-message display smoke.
|
`MsgLib.db` has a validated copied read-only open path through the bundled 32-bit `System.Data.SQLite.dll` and password `123`. C27 adds `MsgLibReadSidecar` as the bounded x86 .NET reader boundary, C28 adds the Go `internal/msglib` process client, C29 adds bounded `display_entities` extraction, C30 wires it as optional contact/group MCP enrichment, C31 proves real copied-DB MCP enrichment with sanitized output, C32 reuses it for receive-message display fields, and C33 proves that receive display path through a real copied-DB MCP smoke without printing entity values. The next step is an operator runbook/setup smoke for the optional enrichment path.
|
||||||
|
|||||||
@@ -2482,17 +2482,56 @@ Receive-message display-name enrichment result:
|
|||||||
- Optional smoke may internally use real enriched IDs but should print only booleans/counts/source refs.
|
- Optional smoke may internally use real enriched IDs but should print only booleans/counts/source refs.
|
||||||
- Do not print entity values, message bodies, file paths, or raw rows.
|
- Do not print entity values, message bodies, file paths, or raw rows.
|
||||||
|
|
||||||
- [ ] **Step 1: Add optional receive display smoke**
|
- [x] **Step 1: Add optional receive display smoke**
|
||||||
|
|
||||||
Use real MsgLib-enriched contact/group metadata internally to construct a synthetic encrypted PacketReader fixture and assert display fields are populated.
|
Extended `scripts/verify-msglib-mcp-enrichment.ps1` so it internally selects real MsgLib contact/group display metadata, constructs a synthetic encrypted PacketReader fixture, starts MCP with both MsgLib and packet-log env, and asserts `sender_name` plus `conversation.display_name` are populated. The final JSON prints only counts, booleans, and `msglib:<source_table>` refs.
|
||||||
|
|
||||||
- [ ] **Step 2: Run full and optional verification**
|
- [x] **Step 2: Run full and optional verification**
|
||||||
|
|
||||||
Run standard gate plus optional smoke with copied DB env.
|
Verified RED first: after adding the new PowerShell assertions, the old script failed with `receive display smoke should be true`. Verified GREEN with copied DB env: `receive_display_smoke=true`, `receive_message_count=2`, `receive_sender_name_populated=true`, `receive_conversation_display_populated=true`, `printed_entity_values=false`, `message_bodies_returned=false`, `file_paths_returned=false`, and `raw_rows_returned=false`.
|
||||||
|
|
||||||
- [ ] **Step 3: Update docs and commit**
|
- [x] **Step 3: Update docs and commit**
|
||||||
|
|
||||||
Record sanitized evidence and next loop.
|
Updated status, capability matrix, sidecar contract, and this plan. Commit/push are handled after the verification gate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Loop C33 Result
|
||||||
|
|
||||||
|
Optional real receive-message display smoke result:
|
||||||
|
|
||||||
|
- `scripts/verify-msglib-mcp-enrichment.ps1` now covers both search enrichment and receive-message display enrichment.
|
||||||
|
- The smoke internally uses real copied-DB MsgLib contact/group metadata to build a synthetic encrypted PacketReader fixture, then verifies MCP receive output populates `sender_name` and `conversation.display_name`.
|
||||||
|
- Printed evidence remains sanitized: counts, booleans, and `msglib:<source_table>` refs only.
|
||||||
|
- It does not print entity values, JIDs, message bodies, file paths, or raw rows.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Loop C34: MsgLib enrichment operator runbook and setup smoke
|
||||||
|
|
||||||
|
**Goal:** Make the optional MsgLib enrichment path operator-usable by documenting the required environment variables, verification commands, safe output expectations, and fallback behavior when MsgLib env is absent.
|
||||||
|
|
||||||
|
**Planned files:**
|
||||||
|
- Modify: `docs/go-mcp-runbook.md`.
|
||||||
|
- Modify: `docs/current-status-card.md`, `docs/source-discovery/capability-source-matrix.md`, and this plan.
|
||||||
|
- Optionally modify verification scripts only if the runbook reveals a setup gap.
|
||||||
|
|
||||||
|
**Planned behavior:**
|
||||||
|
- Standard MCP verification stays deterministic and clears MsgLib env.
|
||||||
|
- Optional MsgLib enrichment remains opt-in through `ISPHERE_MSGLIB_SIDECAR_EXE`, `ISPHERE_MSGLIB_SQLITE_DLL`, and `ISPHERE_MSGLIB_DB`.
|
||||||
|
- Operator-facing docs must explain expected sanitized evidence and no-value output boundaries.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Audit current runbook for MsgLib setup gaps**
|
||||||
|
|
||||||
|
Review `docs/go-mcp-runbook.md` against the C27-C33 implementation and note missing operator steps.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add operator setup and verification section**
|
||||||
|
|
||||||
|
Document build, env config, default deterministic verification, optional copied-DB smoke, and expected sanitized output.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run verification and update next loop**
|
||||||
|
|
||||||
|
Run the full gate, record C34 outcome, and choose the next business slice.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ try {
|
|||||||
Push-Location $repo
|
Push-Location $repo
|
||||||
try {
|
try {
|
||||||
if (-not $SkipBuild) {
|
if (-not $SkipBuild) {
|
||||||
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $repo "scripts\build-msglib-sidecar.ps1") | Out-Host
|
$buildOutput = & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $repo "scripts\build-msglib-sidecar.ps1") 2>&1
|
||||||
if ($LASTEXITCODE -ne 0) { throw "build-msglib-sidecar.ps1 failed with exit code $LASTEXITCODE" }
|
if ($LASTEXITCODE -ne 0) { throw "build-msglib-sidecar.ps1 failed with exit code $LASTEXITCODE" }
|
||||||
}
|
}
|
||||||
Assert-True (Test-Path -LiteralPath $sidecarPath) "sidecar not found: $sidecarPath"
|
Assert-True (Test-Path -LiteralPath $sidecarPath) "sidecar not found: $sidecarPath"
|
||||||
@@ -39,10 +39,14 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/des"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
@@ -53,10 +57,18 @@ import (
|
|||||||
type summary struct {
|
type summary struct {
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
Sources []string `json:"sources"`
|
Sources []string `json:"sources"`
|
||||||
|
FirstID string
|
||||||
|
FirstDisplayName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type receiveDisplaySummary struct {
|
||||||
|
MessageCount int
|
||||||
|
SenderNamePopulated bool
|
||||||
|
ConversationDisplayPopulated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
serverTransport, clientTransport := mcp.NewInMemoryTransports()
|
serverTransport, clientTransport := mcp.NewInMemoryTransports()
|
||||||
@@ -82,6 +94,13 @@ func main() {
|
|||||||
groups := callSearch(ctx, session, "isphere_search_groups", "groups")
|
groups := callSearch(ctx, session, "isphere_search_groups", "groups")
|
||||||
if contacts.Count == 0 { fail("contacts", fmt.Errorf("no msglib_readonly contacts returned")) }
|
if contacts.Count == 0 { fail("contacts", fmt.Errorf("no msglib_readonly contacts returned")) }
|
||||||
if groups.Count == 0 { fail("groups", fmt.Errorf("no msglib_readonly groups returned")) }
|
if groups.Count == 0 { fail("groups", fmt.Errorf("no msglib_readonly groups returned")) }
|
||||||
|
if contacts.FirstID == "" || contacts.FirstDisplayName == "" { fail("contacts", fmt.Errorf("contact display fixture unavailable")) }
|
||||||
|
if groups.FirstID == "" || groups.FirstDisplayName == "" { fail("groups", fmt.Errorf("group display fixture unavailable")) }
|
||||||
|
|
||||||
|
receiveDisplay := verifyReceiveDisplay(ctx, contacts, groups)
|
||||||
|
if receiveDisplay.MessageCount == 0 { fail("receive_display", fmt.Errorf("receive message fixture returned no messages")) }
|
||||||
|
if !receiveDisplay.SenderNamePopulated { fail("receive_display", fmt.Errorf("sender display was not populated")) }
|
||||||
|
if !receiveDisplay.ConversationDisplayPopulated { fail("receive_display", fmt.Errorf("conversation display was not populated")) }
|
||||||
|
|
||||||
out := map[string]any{
|
out := map[string]any{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
@@ -90,8 +109,13 @@ func main() {
|
|||||||
"contact_sources": contacts.Sources,
|
"contact_sources": contacts.Sources,
|
||||||
"group_count": groups.Count,
|
"group_count": groups.Count,
|
||||||
"group_sources": groups.Sources,
|
"group_sources": groups.Sources,
|
||||||
|
"receive_display_smoke": true,
|
||||||
|
"receive_message_count": receiveDisplay.MessageCount,
|
||||||
|
"receive_sender_name_populated": receiveDisplay.SenderNamePopulated,
|
||||||
|
"receive_conversation_display_populated": receiveDisplay.ConversationDisplayPopulated,
|
||||||
"printed_entity_values": false,
|
"printed_entity_values": false,
|
||||||
"message_bodies_returned": false,
|
"message_bodies_returned": false,
|
||||||
|
"file_paths_returned": false,
|
||||||
"raw_rows_returned": false,
|
"raw_rows_returned": false,
|
||||||
}
|
}
|
||||||
encoded, _ := json.Marshal(out)
|
encoded, _ := json.Marshal(out)
|
||||||
@@ -111,9 +135,12 @@ func callSearch(ctx context.Context, session *mcp.ClientSession, tool string, fi
|
|||||||
var decoded map[string]any
|
var decoded map[string]any
|
||||||
if err := json.Unmarshal(payload, &decoded); err != nil { fail(tool+"_decode", err) }
|
if err := json.Unmarshal(payload, &decoded); err != nil { fail(tool+"_decode", err) }
|
||||||
values, ok := decoded[field].([]any)
|
values, ok := decoded[field].([]any)
|
||||||
if !ok { fail(tool+"_shape", fmt.Errorf("%s missing or not array", field)) }
|
if !ok { fail(tool+"_shape", fmt.Errorf("result field missing or not array")) }
|
||||||
|
idKey := "contact_id"
|
||||||
|
if field == "groups" { idKey = "group_id" }
|
||||||
sourceSet := map[string]bool{}
|
sourceSet := map[string]bool{}
|
||||||
count := 0
|
count := 0
|
||||||
|
out := summary{}
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
item, ok := value.(map[string]any)
|
item, ok := value.(map[string]any)
|
||||||
if !ok { continue }
|
if !ok { continue }
|
||||||
@@ -121,12 +148,140 @@ func callSearch(ctx context.Context, session *mcp.ClientSession, tool string, fi
|
|||||||
if source != "msglib_readonly" { continue }
|
if source != "msglib_readonly" { continue }
|
||||||
rawRef, _ := item["raw_ref"].(string)
|
rawRef, _ := item["raw_ref"].(string)
|
||||||
if rawRef != "" { sourceSet[rawRef] = true }
|
if rawRef != "" { sourceSet[rawRef] = true }
|
||||||
|
id, _ := item[idKey].(string)
|
||||||
|
displayName, _ := item["display_name"].(string)
|
||||||
|
if out.FirstID == "" && strings.TrimSpace(id) != "" && strings.TrimSpace(displayName) != "" {
|
||||||
|
out.FirstID = strings.TrimSpace(id)
|
||||||
|
out.FirstDisplayName = strings.TrimSpace(displayName)
|
||||||
|
}
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
sources := make([]string, 0, len(sourceSet))
|
sources := make([]string, 0, len(sourceSet))
|
||||||
for source := range sourceSet { sources = append(sources, source) }
|
for source := range sourceSet { sources = append(sources, source) }
|
||||||
sort.Strings(sources)
|
sort.Strings(sources)
|
||||||
return summary{Count: count, Sources: sources}
|
out.Count = count
|
||||||
|
out.Sources = sources
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyReceiveDisplay(ctx context.Context, contact summary, group summary) receiveDisplaySummary {
|
||||||
|
groupPlaintext := packetFixturePlaintext("c33-group-display-fixture", group.FirstID+"/imp_pc_4.1.2.6842", contact.FirstID, "groupchat", "redacted-group-body")
|
||||||
|
directPlaintext := packetFixturePlaintext("c33-contact-display-fixture", contact.FirstID+"/imp_pc_4.1.2.6842", "receiver-redacted@imopenfire1-lanzhou", "chat", "redacted-contact-body")
|
||||||
|
groupLine, err := encryptPacketLogLineForHarness(groupPlaintext)
|
||||||
|
if err != nil { fail("receive_display_encrypt", err) }
|
||||||
|
directLine, err := encryptPacketLogLineForHarness(directPlaintext)
|
||||||
|
if err != nil { fail("receive_display_encrypt", err) }
|
||||||
|
|
||||||
|
file, err := os.CreateTemp("", "isphere-msglib-receive-display-*.txt")
|
||||||
|
if err != nil { fail("receive_display_fixture", err) }
|
||||||
|
fixturePath := file.Name()
|
||||||
|
defer os.Remove(fixturePath)
|
||||||
|
if _, err := file.WriteString("\n" + groupLine + "\n" + directLine + "\n"); err != nil {
|
||||||
|
_ = file.Close()
|
||||||
|
fail("receive_display_fixture", err)
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil { fail("receive_display_fixture", err) }
|
||||||
|
|
||||||
|
oldFileValue, hadOldFileValue := os.LookupEnv("ISPHERE_PACKET_LOG_FILE")
|
||||||
|
oldDirValue, hadOldDirValue := os.LookupEnv("ISPHERE_PACKET_LOG_DIR")
|
||||||
|
_ = os.Unsetenv("ISPHERE_PACKET_LOG_DIR")
|
||||||
|
if err := os.Setenv("ISPHERE_PACKET_LOG_FILE", fixturePath); err != nil { fail("receive_display_env", err) }
|
||||||
|
defer func() {
|
||||||
|
if hadOldFileValue { _ = os.Setenv("ISPHERE_PACKET_LOG_FILE", oldFileValue) } else { _ = os.Unsetenv("ISPHERE_PACKET_LOG_FILE") }
|
||||||
|
if hadOldDirValue { _ = os.Setenv("ISPHERE_PACKET_LOG_DIR", oldDirValue) } else { _ = os.Unsetenv("ISPHERE_PACKET_LOG_DIR") }
|
||||||
|
}()
|
||||||
|
|
||||||
|
serverCtx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
serverTransport, clientTransport := mcp.NewInMemoryTransports()
|
||||||
|
server, err := mcpserver.NewServerFromEnv()
|
||||||
|
if err != nil { fail("receive_display_server", err) }
|
||||||
|
serverErr := make(chan error, 1)
|
||||||
|
go func() { serverErr <- server.Run(serverCtx, serverTransport) }()
|
||||||
|
|
||||||
|
client := mcp.NewClient(&mcp.Implementation{Name: "verify-msglib-mcp-receive-display", Version: "0.1.0"}, nil)
|
||||||
|
session, err := client.Connect(serverCtx, clientTransport, nil)
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
fail("receive_display_connect", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = session.Close()
|
||||||
|
cancel()
|
||||||
|
select {
|
||||||
|
case <-serverErr:
|
||||||
|
case <-time.After(2*time.Second):
|
||||||
|
fail("receive_display_shutdown", fmt.Errorf("server did not stop"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
result, err := session.CallTool(serverCtx, &mcp.CallToolParams{Name: "isphere_receive_messages", Arguments: map[string]any{
|
||||||
|
"source_preference": "auto",
|
||||||
|
"preview": true,
|
||||||
|
"cursor": "",
|
||||||
|
"include_attachment_metadata": false,
|
||||||
|
"limit": 5,
|
||||||
|
}})
|
||||||
|
if err != nil { fail("receive_display_call", err) }
|
||||||
|
if result.IsError { fail("receive_display_call", fmt.Errorf("tool returned isError=true")) }
|
||||||
|
payload, _ := json.Marshal(result.StructuredContent)
|
||||||
|
var decoded map[string]any
|
||||||
|
if err := json.Unmarshal(payload, &decoded); err != nil { fail("receive_display_decode", err) }
|
||||||
|
messages, ok := decoded["messages"].([]any)
|
||||||
|
if !ok { fail("receive_display_shape", fmt.Errorf("messages missing or not array")) }
|
||||||
|
|
||||||
|
out := receiveDisplaySummary{MessageCount: len(messages)}
|
||||||
|
if conversation, ok := decoded["conversation"].(map[string]any); ok {
|
||||||
|
displayName, _ := conversation["display_name"].(string)
|
||||||
|
out.ConversationDisplayPopulated = displayName == group.FirstDisplayName
|
||||||
|
}
|
||||||
|
for _, value := range messages {
|
||||||
|
message, ok := value.(map[string]any)
|
||||||
|
if !ok { continue }
|
||||||
|
senderID, _ := message["sender_id"].(string)
|
||||||
|
senderName, _ := message["sender_name"].(string)
|
||||||
|
if senderID == contact.FirstID && senderName == contact.FirstDisplayName {
|
||||||
|
out.SenderNamePopulated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func packetFixturePlaintext(id string, from string, to string, messageType string, body string) string {
|
||||||
|
return "--------------------------------------------------------------------------------------------------------------------------------------------\n" +
|
||||||
|
"2026/7/10 09:30:00\n" +
|
||||||
|
fmt.Sprintf("<message id=\"%s\" from=\"%s\" to=\"%s\" type=\"%s\">\n", xmlAttr(id), xmlAttr(from), xmlAttr(to), xmlAttr(messageType)) +
|
||||||
|
fmt.Sprintf(" <body>%s</body>\n", xmlText(body)) +
|
||||||
|
" <received xmlns=\"urn:xmpp:receipts\" id=\"receipt-c33-display-fixture\" type=\"1\" stamp=\"\" />\n" +
|
||||||
|
" <subject>DISPLAY_SMOKE</subject>\n" +
|
||||||
|
" <isphere xmlns=\"isphere.im\" type=\"1001\" sendtime=\"1783423807000\" version=\"1\" />\n" +
|
||||||
|
" <readed />\n" +
|
||||||
|
"</message>"
|
||||||
|
}
|
||||||
|
|
||||||
|
func xmlAttr(value string) string {
|
||||||
|
return strings.NewReplacer("&", "&", "\"", """, "<", "<", ">", ">").Replace(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func xmlText(value string) string {
|
||||||
|
return strings.NewReplacer("&", "&", "<", "<", ">", ">").Replace(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptPacketLogLineForHarness(plaintext string) (string, error) {
|
||||||
|
block, err := des.NewCipher([]byte("hyhccdtm"))
|
||||||
|
if err != nil { return "", err }
|
||||||
|
plain := padPKCS7ForHarness([]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 padPKCS7ForHarness(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 fail(step string, err error) {
|
func fail(step string, err error) {
|
||||||
@@ -163,6 +318,9 @@ func fail(step string, err error) {
|
|||||||
Assert-True ($json.printed_entity_values -eq $false) "harness must not print entity values"
|
Assert-True ($json.printed_entity_values -eq $false) "harness must not print entity values"
|
||||||
Assert-True ($json.contact_count -gt 0) "contact_count should be > 0"
|
Assert-True ($json.contact_count -gt 0) "contact_count should be > 0"
|
||||||
Assert-True ($json.group_count -gt 0) "group_count should be > 0"
|
Assert-True ($json.group_count -gt 0) "group_count should be > 0"
|
||||||
|
Assert-True (($json.PSObject.Properties.Name -contains "receive_display_smoke") -and $json.receive_display_smoke -eq $true) "receive display smoke should be true"
|
||||||
|
Assert-True (($json.PSObject.Properties.Name -contains "receive_sender_name_populated") -and $json.receive_sender_name_populated -eq $true) "receive sender_name should be populated"
|
||||||
|
Assert-True (($json.PSObject.Properties.Name -contains "receive_conversation_display_populated") -and $json.receive_conversation_display_populated -eq $true) "receive conversation.display_name should be populated"
|
||||||
}
|
}
|
||||||
finally { Pop-Location }
|
finally { Pop-Location }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user