chore: package send recording and capability tests

This commit is contained in:
zhaoyilun
2026-07-10 14:36:15 +08:00
parent 8cdaa3c0bc
commit 89d7f65b45
10 changed files with 1000 additions and 14 deletions

View File

@@ -0,0 +1,585 @@
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_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"`
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"`
}
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()
auditPath := filepath.Join("runs", "capability-smoke", "send-preview.jsonl")
if err := os.Setenv("ISPHERE_SEND_AUDIT_PATH", auditPath); 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
}
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,
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,
}
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",
} {
_ = 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 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 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)
}

View File

@@ -58,7 +58,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme
## Current loop
Current loop: `R6c local online-sandbox evidence package prepared; next R6d returned sandbox evidence intake after package is run and returned`.
Current loop: `R6c local online-sandbox evidence package consolidated plus offline capability-test package prepared; next R6d returned sandbox evidence intake after package is run and returned`.
Plan file: `docs/superpowers/plans/2026-07-10-core-business-capabilities-roadmap.md`.
@@ -116,6 +116,7 @@ Current loop output so far:
43. R6a/C31: added WinHelper `0.4.0` read-only `probe_send_entrypoints` and `probe_send_uia_controls`, wired the live recorder to emit `send_entrypoints_preflight.json` and `send_uia_controls_preflight.json`, and verified that copied install metadata reports all 5 required B-route entrypoints available while the synthetic UIA classifier reports `A_ROUTE_OFFLINE_BLOCKED`. Production send/file upload remains blocked pending preview/dry-run, dynamic observation, idempotency/audit, and one approved sandbox send.
44. R6b: added `isphere_send_message` preview/dry-run only. The tool validates `target_type`, `target_id`, `content_sha256`, and `idempotency_key`, returns connector metadata `implatform-sidecar`/`preview`, appends redacted JSONL audit events without raw message body or raw idempotency key, and returns `send_status="blocked"` for production with all side-effect flags false. `verify-go-mcp.ps1` now verifies the 9-tool surface and proves `send_preview_tool_present=true` and `production_send_enabled=false`.
45. R6c: because the local machine cannot log in, prepared an online/internal sandbox evidence package instead of attempting local send. Added `scripts/package-send-sandbox-gate.ps1`, `scripts/verify-send-sandbox-gate-package.ps1`, and `docs/source-discovery/2026-07-10-send-sandbox-gate.md`; local verification generates `runs/send-sandbox-gate-package.zip` with the read-only recorder, before/after scripts, sandbox input template, SHA256 helper, expected-return checklist, and return-zip helper.
46. R6c package consolidation: upgraded `runs/send-sandbox-gate-package.zip` into a one-pass operator recording suite with `RUN-RECORDING-SUITE.bat`, numbered `01/02/03` scripts, and `RECORDING-PACKAGE-MANIFEST.json`; added a second offline-safe `runs/send-capability-test-package.zip` with `isphere-capability-smoke.exe` plus packaged `ISphereWinHelper.exe` to verify the 9-tool MCP surface, receive/search/file-list fixture behavior, send preview, and `production_send_enabled=false`.
## Recompiled business roadmap
@@ -194,6 +195,9 @@ R6c local preparation for online sandbox-send evidence is complete:
- Added package script `scripts/package-send-sandbox-gate.ps1`.
- Added verification script `scripts/verify-send-sandbox-gate-package.ps1`.
- Verified package output: `runs/send-sandbox-gate-package.zip`.
- Package requires an online/internal sandbox operator to run before/after read-only recorder captures around exactly one manual sandbox send.
- Package now includes an all-in-one `RUN-RECORDING-SUITE.bat`, numbered `01/02/03` scripts, and a manifest so the online/internal sandbox operator can execute the full recording/return flow from one extracted folder.
- Added `scripts/package-send-capability-test.ps1`, `scripts/verify-send-capability-test-package.ps1`, and `cmd/isphere-capability-smoke`.
- Verified second package output: `runs/send-capability-test-package.zip`.
- The capability-test package is offline-safe and confirms 9 MCP tools, fixture-backed receive/search/file-list behavior, send preview, no real send, and `production_send_enabled=false`.
- Production `isphere_send_message` and `isphere_send_file` remain blocked until the returned package proves success/ack or sent-record evidence and no second send.
- Next node: R6d returned sandbox evidence intake after the package is run and returned.

View File

@@ -390,16 +390,57 @@ runs\send-sandbox-gate-package.zip
Send that zip to the online/internal sandbox operator. The operator must follow `OPERATOR-STEPS.md` inside the package:
1. fill `SANDBOX-SEND-INPUTS.json`;
2. run `run-before-recorder.bat`;
2. run `RUN-RECORDING-SUITE.bat` for the guided full flow, or run `01-run-before-recorder.bat`;
3. manually send exactly one sandbox message in the official client;
4. fill `success_ack_or_sent_record`;
5. run `run-after-recorder.bat`;
6. run `make-return-zip.ps1`;
5. continue the guided flow, or run `02-run-after-recorder.bat`;
6. continue the guided flow, or run `03-make-return-zip.bat`;
7. return the generated zip.
The package also contains `RECORDING-PACKAGE-MANIFEST.json`, `run-before-recorder.bat`, `run-after-recorder.bat`, `compute-content-hash.ps1`, `make-return-zip.ps1`, and the read-only recorder under `recorder\`.
Production `isphere_send_message` remains blocked until the returned package is analyzed.
## 14. Troubleshooting
## 14. Offline capability-test package
Use this second package when you want to test the current capability surface without logging in and without any real send.
Build and verify it locally:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\package-send-capability-test.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-send-capability-test-package.ps1
```
Generated package:
```text
runs\send-capability-test-package.zip
```
Inside the extracted package, run:
```text
run-capability-test.bat
```
Expected final JSON includes:
```json
{
"ok": true,
"tool_count": 9,
"helper_name": "ISphereWinHelper",
"send_preview_tool_present": true,
"production_send_enabled": false,
"local_login_required": false,
"no_real_send": true
}
```
This package includes `isphere-capability-smoke.exe` and `runs\win-helper\ISphereWinHelper.exe`. It verifies the nine-tool MCP surface, fixture-backed receive/search/file-list behavior, send preview, and the production-send block. It does not log in, click, type, upload, hook, inject, or send.
## 15. Troubleshooting
- If C# helper build fails, run `scripts\build-win-helper.ps1` directly and check for missing .NET Framework reference assemblies.
- If `win_helper_version` fails, rerun `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-win-helper.ps1` first.

View File

@@ -30,6 +30,20 @@ runs/send-sandbox-gate-package/
runs/send-sandbox-gate-package.zip
```
Companion capability-test package:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\package-send-capability-test.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-send-capability-test-package.ps1
```
```text
runs/send-capability-test-package/
runs/send-capability-test-package.zip
```
The capability-test package is local/offline safe. It runs `isphere-capability-smoke.exe`, confirms the nine-tool MCP surface, exercises receive/search/file-list against synthetic fixtures, verifies send preview, and confirms `production_send_enabled=false`.
The package embeds the existing read-only live probe recorder under:
```text
@@ -45,6 +59,11 @@ recorder/variants/ISphereLiveProbeRecorder-x86.exe
| `OPERATOR-STEPS.md` | Step-by-step operator procedure. |
| `SANDBOX-SEND-INPUTS.template.json` | Template for sandbox target, content hash, idempotency key, and observed result. |
| `EXPECTED-RETURN-FILES.txt` | Return package acceptance checklist. |
| `RUN-RECORDING-SUITE.bat` | Top-level guided before/manual-send/after/return-zip flow. |
| `01-run-before-recorder.bat` | Numbered wrapper for before recording. |
| `02-run-after-recorder.bat` | Numbered wrapper for after recording. |
| `03-make-return-zip.bat` | Numbered wrapper for return zip creation. |
| `RECORDING-PACKAGE-MANIFEST.json` | Machine-readable package manifest and one-send markers. |
| `run-before-recorder.bat` | Runs read-only recorder before the manual sandbox send. |
| `run-after-recorder.bat` | Runs read-only recorder after the manual sandbox send. |
| `compute-content-hash.ps1` | Computes SHA256 for the exact test message body. |
@@ -68,12 +87,12 @@ Required online procedure:
1. Log in normally on an online/internal sandbox machine.
2. Fill `SANDBOX-SEND-INPUTS.json` from the template.
3. Run `run-before-recorder.bat`.
3. Run `RUN-RECORDING-SUITE.bat` for the guided all-in-one flow, or run `01-run-before-recorder.bat`.
4. Manually send exactly one message to the declared sandbox target.
5. Record `success_ack_or_sent_record`.
6. Do not send a second copy.
7. Run `run-after-recorder.bat`.
8. Run `make-return-zip.ps1`.
7. Continue the guided flow, or run `02-run-after-recorder.bat`.
8. Continue the guided flow, or run `03-make-return-zip.bat`.
9. Return the generated zip.
## Safety boundary
@@ -102,7 +121,7 @@ R6c can pass only after a returned package proves all of the following:
## Current decision
R6c local-preparation slice is complete, but the full online evidence gate is not passed yet.
R6c local-preparation slice is complete, including the consolidated recording suite and the separate offline capability-test package. The full online evidence gate is not passed yet.
Production `isphere_send_message` remains blocked.

View File

@@ -25,7 +25,7 @@ Send sandbox gate: `docs/source-discovery/2026-07-10-send-sandbox-gate.md`
| `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`; `internal/isphere/contacts_test.go`; `internal/tools/isphere_contacts_test.go` | C34 documents optional MsgLib contact display enrichment; R2 adds deterministic exact-match-first ranking, case-insensitive de-duplication across log/MsgLib candidates, and preserves `source`/`raw_ref` | core contact search complete; optional richer fields later |
| `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`; `internal/isphere/groups_test.go`; `internal/tools/isphere_groups_test.go`; C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C34 documents optional MsgLib group display enrichment; R2 adds deterministic exact-match-first ranking, case-insensitive de-duplication across log/MsgLib candidates, and preserves `source`/`raw_ref` | core group search complete; optional member/owner hierarchy later |
| `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 | MsgLib `TD_ReceiveFileRecord` safe metadata through explicit copied-DB path; 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`; `docs/source-discovery/2026-07-10-file-download-mapping-precheck.md`; `docs/source-discovery/2026-07-10-file-cache-mapping-diagnostic.md` | C22 verifies safe file-list contract args; R3 confirms list metadata is available; R3b diagnostic finds 0 current cache/archive matches, so R4 remains blocked | blocked pending better cache evidence or UI/client download connector |
| `isphere_send_message` | B-route selected: running-client sidecar / in-process connector, preferring `AppContextManager.SendTxtMessageByJid(...)` or `MessageScheduling.SendChatMessage(...)` over the unimplemented `SendP2PMessage(...)` interface path; current MCP tool exposes preview/dry-run only | A-route fallback: constrained UI/RPA wrapper only if B cannot pass runtime probe, entrypoint availability, dynamic observation, and idempotent dry-run gates; network/protocol replay remains deferred | `docs/source-discovery/2026-07-10-send-message-source-precheck.md`; `docs/source-discovery/2026-07-10-send-message-connector-selection.md`; `docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md`; `docs/source-discovery/2026-07-10-returned-live-probe-analysis.md`; `docs/source-discovery/2026-07-10-send-connector-preflight.md`; `docs/source-discovery/2026-07-10-send-sandbox-gate.md`; `internal/tools/isphere_send_message_test.go` | R6c prepares the online sandbox evidence package because local login is unavailable. `runs/send-sandbox-gate-package.zip` can collect before/after read-only recorder outputs around one manual sandbox send, but no returned evidence has been analyzed yet. Production remains blocked. | next: R6d returned sandbox evidence intake after user returns the package |
| `isphere_send_message` | B-route selected: running-client sidecar / in-process connector, preferring `AppContextManager.SendTxtMessageByJid(...)` or `MessageScheduling.SendChatMessage(...)` over the unimplemented `SendP2PMessage(...)` interface path; current MCP tool exposes preview/dry-run only | A-route fallback: constrained UI/RPA wrapper only if B cannot pass runtime probe, entrypoint availability, dynamic observation, and idempotent dry-run gates; network/protocol replay remains deferred | `docs/source-discovery/2026-07-10-send-message-source-precheck.md`; `docs/source-discovery/2026-07-10-send-message-connector-selection.md`; `docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md`; `docs/source-discovery/2026-07-10-returned-live-probe-analysis.md`; `docs/source-discovery/2026-07-10-send-connector-preflight.md`; `docs/source-discovery/2026-07-10-send-sandbox-gate.md`; `internal/tools/isphere_send_message_test.go`; `cmd/isphere-capability-smoke/main.go` | R6c prepares the online sandbox evidence package because local login is unavailable. `runs/send-sandbox-gate-package.zip` now contains a consolidated recording suite for one manual sandbox send; `runs/send-capability-test-package.zip` separately verifies the offline-safe 9-tool surface, fixture-backed receive/search/file-list behavior, send preview, and `production_send_enabled=false`. No returned online send evidence has been analyzed yet. Production remains blocked. | next: R6d returned sandbox evidence intake after user returns the package |
| `isphere_send_file` | B-route selected after send-message probe: managed offline-file path through `OffLineFileSend.sendFile()`, `IMPP.Service*.dll` `IFileTransfer.FileUpload(...)`, and `Chat.sendFileMessage(...)` | A-route fallback: constrained UI/RPA upload/send-file flow; native `TcpFileTransfer.dll` is a later candidate only after managed path is rejected | `docs/source-discovery/2026-07-10-send-message-source-precheck.md`; `docs/source-discovery/2026-07-10-send-message-connector-selection.md`; `docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md`; `docs/source-discovery/2026-07-10-returned-live-probe-analysis.md`; `docs/source-discovery/2026-07-10-send-connector-preflight.md`; `docs/source-discovery/2026-07-10-send-sandbox-gate.md` | R6a/C31 metadata confirms `FileTransfer.FileUpload`, `FileTransfer.AsynFileUpload`, `OffLineFileSend.sendFile`, and `Chat.sendFileMessage` candidates. File-send remains blocked behind returned message-send sandbox evidence plus later upload/file-message dry-run evidence. | next: wait for R6d, then file-send upload preflight |
## Stage C entry rule
@@ -43,4 +43,4 @@ Start Stage C with a narrow log-backed `isphere_receive_messages` source abstrac
## 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, C32 reuses it for receive-message display fields, C33 proves that receive display path through a real copied-DB MCP smoke without printing entity values, C34 documents the operator setup/verification path, C35 maps MsgLib message tables to receive-message contract fields without reading row values, C36 adds metadata-only `message_sources` readiness, C37 defines the bounded DB-backed receive-source design, C38 implements sidecar/Go-wrapper `list_messages` with sanitized verification, C39 adds a Go adapter from MsgLib list results to the receive-message domain model, C40 adds explicit tool-level `msglib_readonly` source selection, C41 wires env-configured explicit MsgLib receive with optional sanitized smoke, R1 documents source reconciliation keys, and R2 hardens contact/group search ranking and de-duplication. The active roadmap is `docs/superpowers/plans/2026-07-10-core-business-capabilities-roadmap.md`; next step is R6d returned sandbox evidence intake after the online package is returned.
`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, C33 proves that receive display path through a real copied-DB MCP smoke without printing entity values, C34 documents the operator setup/verification path, C35 maps MsgLib message tables to receive-message contract fields without reading row values, C36 adds metadata-only `message_sources` readiness, C37 defines the bounded DB-backed receive-source design, C38 implements sidecar/Go-wrapper `list_messages` with sanitized verification, C39 adds a Go adapter from MsgLib list results to the receive-message domain model, C40 adds explicit tool-level `msglib_readonly` source selection, C41 wires env-configured explicit MsgLib receive with optional sanitized smoke, R1 documents source reconciliation keys, and R2 hardens contact/group search ranking and de-duplication. R6c now has both the consolidated online recording package and the offline capability-test package. The active roadmap is `docs/superpowers/plans/2026-07-10-core-business-capabilities-roadmap.md`; next step is R6d returned sandbox evidence intake after the online package is returned.

View File

@@ -28,7 +28,7 @@
| Search contacts | Registered MCP tool; deterministic exact-match ranking and case-insensitive de-duplication across log/JID candidates and optional copied-DB MsgLib display enrichment. | Optional richer fields such as department/title remain future enrichment, not a core search blocker. | complete for core search; optional enrichment later |
| Search groups | Registered MCP tool; deterministic exact-match ranking and case-insensitive de-duplication across groupchat/conference candidates and optional copied-DB MsgLib display enrichment. | Optional member count, owner refs, and nested/group hierarchy remain future enrichment, not a core search blocker. | complete for core search; optional enrichment later |
| Receive messages | PacketReader source works; copied `MsgLib.db` explicit receive works with `source_preference="msglib_readonly"`; R1 precheck says default should remain PacketReader until a future reconciliation helper is implemented. | Future reconciliation helper, source confidence scoring, pagination policy. | future receive helper after R2/R3 |
| Send messages | `isphere_send_message` preview/dry-run is registered; R6a confirms B-route entrypoint metadata; R6c local package for online sandbox evidence is prepared. | Need returned online sandbox evidence proving success/ack and no duplicate send before production. | next R6d returned sandbox evidence intake |
| Send messages | `isphere_send_message` preview/dry-run is registered; R6a confirms B-route entrypoint metadata; R6c has a consolidated online sandbox recording package plus a separate offline capability-test package. | Need returned online sandbox evidence proving success/ack and no duplicate send before production. | next R6d returned sandbox evidence intake |
| Receive files | List mode exists from message-derived file metadata; R3/R3b confirm DB file metadata exists but current cache/archive evidence does not map to local files. | R4 download is blocked until better cache evidence or a UI/client download connector is available. | blocked; continue R5 |
| Send files | Contract exists; R6a/C31 metadata confirms file-upload/offline-file/file-message candidates and A-route file menu classifier support. | Blocked behind returned send-message sandbox evidence plus upload/file-transfer dry-run. | wait for R6d, then file-send upload preflight |
@@ -480,7 +480,9 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-go-mcp.ps1
- Added `scripts/package-send-sandbox-gate.ps1`.
- Added `scripts/verify-send-sandbox-gate-package.ps1`.
- Generated and locally verified `runs/send-sandbox-gate-package.zip`.
- The package includes the read-only live probe recorder plus before/after recorder batch files, sandbox input template, SHA256 helper, expected-return checklist, and return-zip script.
- The package includes the read-only live probe recorder plus before/after recorder batch files, sandbox input template, SHA256 helper, expected-return checklist, return-zip script, `RUN-RECORDING-SUITE.bat`, numbered `01/02/03` step scripts, and `RECORDING-PACKAGE-MANIFEST.json`.
- Added `cmd/isphere-capability-smoke`, `scripts/package-send-capability-test.ps1`, and `scripts/verify-send-capability-test-package.ps1`.
- Generated and locally verified the companion offline-safe package `runs/send-capability-test-package.zip`; it embeds `isphere-capability-smoke.exe` and `runs/win-helper/ISphereWinHelper.exe`, verifies the nine-tool MCP surface, fixture-backed receive/search/file-list behavior, send preview, and `production_send_enabled=false`.
- This local slice does not pass the online evidence gate because the local machine cannot log in. It only prepares the exact package needed for an online/internal sandbox operator.
- Decision: next node is R6d returned sandbox evidence intake after the package is run and returned. Production `isphere_send_message` and `isphere_send_file` remain blocked.

View File

@@ -0,0 +1,147 @@
param(
[string]$OutputDir = "runs/send-capability-test-package",
[switch]$NoZip
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
$repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
$packageDir = Join-Path $repo $OutputDir
$zipPath = "$packageDir.zip"
$helperRelative = Join-Path $OutputDir "runs\win-helper"
$helperDir = Join-Path $packageDir "runs\win-helper"
$helperExe = Join-Path $helperDir "ISphereWinHelper.exe"
$smokeExe = Join-Path $packageDir "isphere-capability-smoke.exe"
if (Test-Path -LiteralPath $packageDir) {
Remove-Item -LiteralPath $packageDir -Recurse -Force
}
if (Test-Path -LiteralPath $zipPath) {
Remove-Item -LiteralPath $zipPath -Force
}
New-Item -ItemType Directory -Force -Path $packageDir | Out-Null
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $repo "scripts\build-win-helper.ps1") -OutputDir $helperRelative -Platform anycpu
if ($LASTEXITCODE -ne 0) {
throw "build-win-helper.ps1 failed with exit code $LASTEXITCODE"
}
if (-not (Test-Path -LiteralPath $helperExe)) {
throw "helper executable was not created: $helperExe"
}
Push-Location $repo
try {
& go build -o $smokeExe ./cmd/isphere-capability-smoke
if ($LASTEXITCODE -ne 0) {
throw "go build ./cmd/isphere-capability-smoke failed with exit code $LASTEXITCODE"
}
}
finally {
Pop-Location
}
if (-not (Test-Path -LiteralPath $smokeExe)) {
throw "capability smoke executable was not created: $smokeExe"
}
@'
iSphere Send Capability Test Package
LOCAL_LOGIN_UNAVAILABLE: this package does not require local iSphere login.
NO_REAL_SEND: this package does not send real messages or files.
What to run:
1. Extract the zip to a normal folder.
2. Double-click run-capability-test.bat, or run:
.\isphere-capability-smoke.exe
What it tests:
- the MCP server initializes with the expected 9 tools;
- ISphereWinHelper can be called through the packaged helper path;
- receive messages / receive files / search contacts / search groups work against offline fixtures;
- isphere_send_message preview returns planned with no side effects;
- production send returns blocked and production_send_enabled remains false.
What it does not do:
- it does not log in to iSphere;
- it does not click, type, upload, or send;
- it does not hook or inject into the client;
- it does not modify iSphere client data.
Expected final JSON fields:
- ok=true
- tool_count=9
- helper_name=ISphereWinHelper
- send_preview_tool_present=true
- production_send_enabled=false
- local_login_required=false
- no_real_send=true
'@ | Set-Content -LiteralPath (Join-Path $packageDir "README.txt") -Encoding ASCII
@'
Expected capability smoke result:
Required:
- ok = true
- tool_count = 9
- helper_name = ISphereWinHelper
- receive_message_count = 0
- contact_count = 0
- group_count = 0
- file_count = 0
- fixture_receive_message_count = 1
- fixture_contact_count = 1
- fixture_group_count = 1
- fixture_file_count = 1
- fixture_dir_receive_message_count = 1
- fixture_dir_contact_count = 1
- fixture_dir_group_count = 1
- fixture_dir_file_count = 1
- send_preview_tool_present = true
- production_send_enabled = false
- local_login_required = false
- real_isphere_login_required = false
- no_real_send = true
If production_send_enabled is true, treat the package as failed.
If no_real_send is false, treat the package as failed.
'@ | Set-Content -LiteralPath (Join-Path $packageDir "EXPECTED-RESULTS.txt") -Encoding ASCII
@'
@echo off
setlocal
cd /d "%~dp0"
echo Running iSphere capability smoke test...
isphere-capability-smoke.exe
echo exit code: %ERRORLEVEL%
pause
exit /b %ERRORLEVEL%
'@ | Set-Content -LiteralPath (Join-Path $packageDir "run-capability-test.bat") -Encoding ASCII
@'
{
"package": "send-capability-test",
"LOCAL_LOGIN_UNAVAILABLE": true,
"NO_REAL_SEND": true,
"entrypoint": "run-capability-test.bat",
"smoke_exe": "isphere-capability-smoke.exe",
"helper_exe": "runs/win-helper/ISphereWinHelper.exe",
"expected_tool_count": 9,
"production_send_enabled": false
}
'@ | Set-Content -LiteralPath (Join-Path $packageDir "CAPABILITY-TEST-MANIFEST.json") -Encoding ASCII
if (-not $NoZip) {
Compress-Archive -Path (Join-Path $packageDir "*") -DestinationPath $zipPath -Force
}
[ordered]@{
ok = $true
package_dir = (Resolve-Path -LiteralPath $packageDir).Path
zip = if (Test-Path -LiteralPath $zipPath) { (Resolve-Path -LiteralPath $zipPath).Path } else { $null }
helper_exe = (Resolve-Path -LiteralPath $helperExe).Path
smoke_exe = (Resolve-Path -LiteralPath $smokeExe).Path
local_login_required = $false
no_real_send = $true
production_send_enabled = $false
} | ConvertTo-Json -Depth 4 -Compress

View File

@@ -44,6 +44,7 @@ What this package does:
- asks the operator to manually send exactly one sandbox message in the official client;
- runs the same read-only recorder after the manual send;
- zips the returned evidence folder.
- provides RUN-RECORDING-SUITE.bat plus numbered 01/02/03 batch files so the operator can run the whole recording flow from this package.
What this package does not do:
- it does not send messages automatically;
@@ -60,6 +61,7 @@ Required rule:
Return:
- run make-return-zip.ps1 and send back the generated zip.
- preferred one-file flow: run RUN-RECORDING-SUITE.bat and follow the pauses.
'@ | Set-Content -LiteralPath (Join-Path $packageDir "README.txt") -Encoding UTF8
@'
@@ -96,6 +98,10 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\compute-content-hash.ps1 -
3. Do not send anything yet.
4. Run `run-before-recorder.bat`.
Alternative one-file flow:
Run `RUN-RECORDING-SUITE.bat`. It will run the before recorder, pause for the manual one-send action, run the after recorder, and then call the return-zip script.
## Step 3 - Manual one-send action
1. In the official iSphere client, manually send exactly the `content_text_to_send`.
@@ -122,6 +128,8 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\make-return-zip.ps1
Send back the generated zip file.
You can also run `03-make-return-zip.bat`.
## Required returned evidence
The returned evidence should contain:
@@ -205,6 +213,21 @@ echo Done. Check return-evidence\after.
pause
'@ | Set-Content -LiteralPath (Join-Path $packageDir "run-after-recorder.bat") -Encoding ASCII
@'
@echo off
setlocal
cd /d "%~dp0"
call run-before-recorder.bat
'@ | Set-Content -LiteralPath (Join-Path $packageDir "01-run-before-recorder.bat") -Encoding ASCII
@'
@echo off
setlocal
cd /d "%~dp0"
echo Manual one-send action must already be complete before this step.
call run-after-recorder.bat
'@ | Set-Content -LiteralPath (Join-Path $packageDir "02-run-after-recorder.bat") -Encoding ASCII
@'
param(
[Parameter(Mandatory=$true)]
@@ -256,6 +279,59 @@ Compress-Archive -Path $items -DestinationPath $zip -Force
} | ConvertTo-Json -Depth 4 -Compress
'@ | Set-Content -LiteralPath (Join-Path $packageDir "make-return-zip.ps1") -Encoding ASCII
@'
@echo off
setlocal
cd /d "%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0make-return-zip.ps1"
echo exit code: %ERRORLEVEL%
pause
exit /b %ERRORLEVEL%
'@ | Set-Content -LiteralPath (Join-Path $packageDir "03-make-return-zip.bat") -Encoding ASCII
@'
@echo off
setlocal
cd /d "%~dp0"
echo R6c online sandbox-send recording suite
echo.
echo Step 1: BEFORE read-only recorder.
call "%~dp0run-before-recorder.bat"
echo.
echo Step 2: manual one-send action.
echo In the official iSphere client, manually send exactly one sandbox message.
echo Fill SANDBOX-SEND-INPUTS.json before continuing.
pause
echo.
echo Step 3: AFTER read-only recorder.
call "%~dp0run-after-recorder.bat"
echo.
echo Step 4: make return zip.
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0make-return-zip.ps1"
echo exit code: %ERRORLEVEL%
pause
exit /b %ERRORLEVEL%
'@ | Set-Content -LiteralPath (Join-Path $packageDir "RUN-RECORDING-SUITE.bat") -Encoding ASCII
@'
{
"package": "send-sandbox-gate",
"LOCAL_LOGIN_UNAVAILABLE": true,
"ONLINE_SANDBOX_REQUIRED": true,
"MANUAL_ONE_SEND_ONLY": true,
"DO_NOT_SEND_SECOND_TIME": true,
"entrypoint": "RUN-RECORDING-SUITE.bat",
"numbered_steps": [
"01-run-before-recorder.bat",
"02-run-after-recorder.bat",
"03-make-return-zip.bat"
],
"recorder": "recorder/ISphereLiveProbeRecorder.exe",
"return_inputs": "SANDBOX-SEND-INPUTS.json",
"production_send_enabled": false
}
'@ | Set-Content -LiteralPath (Join-Path $packageDir "RECORDING-PACKAGE-MANIFEST.json") -Encoding ASCII
if (-not $NoZip) {
Compress-Archive -Path (Join-Path $packageDir "*") -DestinationPath $zipPath -Force
}

View File

@@ -0,0 +1,106 @@
param(
[string]$OutputDir = "runs/send-capability-test-package"
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
$repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
$packageScript = Join-Path $repo "scripts\package-send-capability-test.ps1"
function Assert-True([bool]$Condition, [string]$Message) {
if (-not $Condition) {
throw $Message
}
}
if (-not (Test-Path -LiteralPath $packageScript)) {
throw "missing package script: $packageScript"
}
$packageRoot = Join-Path $repo $OutputDir
$zipPath = "$packageRoot.zip"
if (Test-Path -LiteralPath $packageRoot) {
Remove-Item -LiteralPath $packageRoot -Recurse -Force
}
if (Test-Path -LiteralPath $zipPath) {
Remove-Item -LiteralPath $zipPath -Force
}
& powershell -NoProfile -ExecutionPolicy Bypass -File $packageScript -OutputDir $OutputDir
if ($LASTEXITCODE -ne 0) {
throw "package-send-capability-test.ps1 failed with exit code $LASTEXITCODE"
}
$requiredFiles = @(
"README.txt",
"EXPECTED-RESULTS.txt",
"run-capability-test.bat",
"isphere-capability-smoke.exe",
"runs\win-helper\ISphereWinHelper.exe"
)
foreach ($relative in $requiredFiles) {
$path = Join-Path $packageRoot $relative
if (-not (Test-Path -LiteralPath $path)) {
throw "missing package file: $relative"
}
}
$readme = Get-Content -LiteralPath (Join-Path $packageRoot "README.txt") -Raw
$expected = Get-Content -LiteralPath (Join-Path $packageRoot "EXPECTED-RESULTS.txt") -Raw
$runner = Get-Content -LiteralPath (Join-Path $packageRoot "run-capability-test.bat") -Raw
foreach ($needle in @(
"LOCAL_LOGIN_UNAVAILABLE",
"NO_REAL_SEND",
"isphere-capability-smoke.exe",
"tool_count",
"production_send_enabled"
)) {
if ($readme -notmatch [regex]::Escape($needle) -and $expected -notmatch [regex]::Escape($needle) -and $runner -notmatch [regex]::Escape($needle)) {
throw "package instructions missing required phrase: $needle"
}
}
if (-not (Test-Path -LiteralPath $zipPath)) {
throw "missing package zip: $zipPath"
}
$smokeOutput = $null
Push-Location $packageRoot
try {
$smokeOutput = & ".\isphere-capability-smoke.exe"
if ($LASTEXITCODE -ne 0) {
throw "isphere-capability-smoke.exe failed with exit code $LASTEXITCODE"
}
}
finally {
Pop-Location
}
$smokeOutput | ForEach-Object { Write-Host $_ }
$lastLine = @($smokeOutput | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) | Select-Object -Last 1
Assert-True (-not [string]::IsNullOrWhiteSpace($lastLine)) "smoke output was empty"
$smokeJson = $lastLine | ConvertFrom-Json
Assert-True ($smokeJson.ok -eq $true) "smoke ok mismatch: $($smokeJson.ok)"
Assert-True ($smokeJson.helper_name -eq "ISphereWinHelper") "smoke helper_name mismatch: $($smokeJson.helper_name)"
Assert-True ($smokeJson.tool_count -eq 9) "smoke tool_count mismatch: $($smokeJson.tool_count)"
Assert-True ($smokeJson.send_preview_tool_present -eq $true) "smoke send_preview_tool_present mismatch: $($smokeJson.send_preview_tool_present)"
Assert-True ($smokeJson.production_send_enabled -eq $false) "smoke production_send_enabled mismatch: $($smokeJson.production_send_enabled)"
Assert-True ($smokeJson.local_login_required -eq $false) "smoke local_login_required mismatch: $($smokeJson.local_login_required)"
Assert-True ($smokeJson.real_isphere_login_required -eq $false) "smoke real_isphere_login_required mismatch: $($smokeJson.real_isphere_login_required)"
Assert-True ($smokeJson.no_real_send -eq $true) "smoke no_real_send mismatch: $($smokeJson.no_real_send)"
[ordered]@{
ok = $true
package_dir = (Resolve-Path -LiteralPath $packageRoot).Path
zip = (Resolve-Path -LiteralPath $zipPath).Path
required_file_count = $requiredFiles.Count
smoke_tool_count = $smokeJson.tool_count
helper_name = $smokeJson.helper_name
send_preview_tool_present = $smokeJson.send_preview_tool_present
production_send_enabled = $smokeJson.production_send_enabled
local_login_required = $false
no_real_send = $true
} | ConvertTo-Json -Depth 6 -Compress

View File

@@ -31,6 +31,11 @@ $requiredFiles = @(
"OPERATOR-STEPS.md",
"SANDBOX-SEND-INPUTS.template.json",
"EXPECTED-RETURN-FILES.txt",
"RUN-RECORDING-SUITE.bat",
"01-run-before-recorder.bat",
"02-run-after-recorder.bat",
"03-make-return-zip.bat",
"RECORDING-PACKAGE-MANIFEST.json",
"run-before-recorder.bat",
"run-after-recorder.bat",
"make-return-zip.ps1",
@@ -54,6 +59,7 @@ foreach ($needle in @(
"ONLINE_SANDBOX_REQUIRED",
"MANUAL_ONE_SEND_ONLY",
"DO_NOT_SEND_SECOND_TIME",
"RUN-RECORDING-SUITE.bat",
"content_sha256",
"idempotency_key",
"success_ack_or_sent_record"