Files
simplex-chat/packages/simplex-chat-mcp/test/unit/log.test.ts
T
Evgeny @ SimpleX Chat be668564df mcp server: files, /leave and library methods (#7962)
* bots, nodejs: document chat, connection, member contact and network commands, typed commands in library

* bots, nodejs: restore apiConnect, wrap domain once, test all connect responses

* nodejs: JSDoc for new methods

* docs: plan for bot API commands and typed Node library methods

* tests: channel prepared with a domain has a verified SimpleX name

* docs: apply review fixes to the plan for bot API commands

* docs: apply second review pass to the plan for bot API commands

* docs: refer the bot API plan to the rewritten test commit

* docs: record decisions in the plan for bot API commands

* docs: plan tests of the generated Python command strings

* python: test the generated strings of the documented commands

* docs: plan for files, /leave and library methods in the MCP server

* docs: wait at stop in the MCP plan

* docs: library methods and /leave mechanics in the MCP plan

* mcp: typed library methods for chat commands

* docs: plan the message information command for bots

* mcp: /leave CHAT owner command to leave a chat at once

* docs: file servers, GIF and video choices decided in the MCP plan

* docs: file mechanics in the MCP plan

* mcp: files of received messages

* bots, nodejs: message information with the file servers for bots

* docs: plan the relay approval option of receiving files in Node

* mcp: download of received files with owner approval

* docs: name the xftp-server test helper in the files plan

* nodejs: receive files without approving unknown relays

* docs: plan the user servers command for bots

* bots, nodejs: user servers and relays by operator for bots

* nodejs: document the message information method

* docs: plan the file upload progress event for bots

* mcp: send files with owner approval, refusing private keys

* bots: file upload progress event for bots

* mcp: /leave template in the agent control chat menu

* docs: sent copies are deleted for contacts too, at sndFileCompleteXFTP

* bots: drop the file upload progress event

* docs: drop the upload progress event plan

* nodejs: document user servers and relay approval of received files

* mcp: download cards name unknown file servers after the upload

* docs: record the wait for the file description in the files plan

* mcp: fixed wait at stop for cancelled cards, without /sql

* docs: record decision C4, a fixed wait at stop, in the files plan

* ci: test the MCP server package on pull requests

* ci: state when the MCP workflow can be dispatched

* ci: run the MCP server tests when its libraries change

* mcp: log the errors of image previews and resizing

* mcp: keep files in transfer in the data folder

* mcp: restrict sent files by the real paths of refused folders

* mcp: validate the file at the send by its inode and change time

* mcp: limit ffmpeg and ffprobe to local files and no input commands

* mcp: ask the owner for unknown file servers; read approvals at creation

* mcp: name downloaded files by their ID; return paths of downloads only

* mcp: end each download with a result for the agent and the card

* mcp: show files above their sender's size limit as failed

* mcp: refuse empty files, as without file and captions too long to send

* mcp: tell the owner about failed uploads and unposted received files

* mcp: delete resized copies left by the last run at start

* mcp: name downloads and leaving in the help and greeting, align docs

* mcp: compute the download path for received files only

* mcp: send a /receive file to the owner under its name

* mcp: keep files received without a download in the chat's folder

* mcp: keep a left chat's files and delete them with /delete CHAT files

* mcp: test that sent images are resized by the core

* mcp: wait 30 minutes for a download's progress before cancelling it

* mcp: end a download as deleted only when its message is not found

* mcp: delete the copy of a /receive file whose post fails

* mcp: count the quoted content in a message's length, as the core sends it

* mcp: name received bytes, unknown-server downloads and left chat files plainly

* mcp: test refused folders, file changes and download approvals

* docs: align MCP server docs with refused folders, leaves and download waits

* mcp: move received files only in full, delete partials only of downloads

* mcp: report a received file missing from its folder as failed

* mcp: move received files of a group's member chats into its folder

* mcp: read a video's preview and duration as an MP4 or QuickTime file

* mcp: 0.2.0 on simplex-chat 7.1.0-beta.10.1

* mcp: file folders by chat type, /delete cancels downloads, files reply

* docs: MCP server specs and plan for 0.2.0

* mcp: /receive gives the agent a file, /forward sends it to the owner

* docs: MCP server specs for /receive and /forward of download cards

* docs: MCP plan records the temp folder command as a library exception

* mcp: /delete cancels resumed downloads; /forward quote fits the limit

* docs: MCP specs and plans for /forward quotes and resumed downloads

* docs: MCP specs name the source commit they are checked against

* docs: MCP PLAN states the /forward quote and move rules

* mcp: a download moves its completed file whatever its size

* mcp: /receive N all covers its card and later files, not pending ones

* docs: MCP files approval scope, reply text and README server wording

* mcp: a download approves relays only for a card naming unknown servers

* mcp: files in member chats of a group are tracked as in the main chat

* docs: MCP relay approval of downloads and files in member chats

* mcp: a rejected file gets a card again under the files approval

* docs: MCP rejected files under the files approval, README scope

* mcp: an offered file without a description gets no card or action

* docs: MCP a file without a description is not downloaded

* mcp: 0.2.0 on simplex-chat 7.1.0-beta.10.2

* ci: test the MCP server as a scope of build-linux
2026-11-06 18:38:34 +00:00

118 lines
9.3 KiB
TypeScript

import {T} from "@simplex-chat/types"
import {chatErrorText} from "../../src/log"
const server = "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"
const relay = "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im"
const agentError = (error: object): T.ChatError => ({type: "errorAgent", agentError: error, agentConnId: ""}) as unknown as T.ChatError
const broker = (brokerErr: object): object => ({type: "BROKER", brokerAddress: server, brokerErr})
const forwarding = (brokerErr: object): object => ({type: "SMP", serverAddress: server, smpErr: {type: "PROXY", proxyErr: {type: "BROKER", brokerErr}}})
const destination = (proxyErr: object): object => ({type: "PROXY", proxyServer: server, relayServer: relay, proxyErr: {type: "protocolError", protocolErr: {type: "PROXY", proxyErr}}})
const domain = {nameTLD: T.SimplexTLD.Simplex, domain: "ridgeline", subDomain: []}
describe("SimpleX errors", () => {
const cases: [string, T.ChatError, string][] = [
["invalid link", {type: "error", errorType: {type: "invalidConnReq"}}, "Invalid connection link: Please check that you used the correct link or ask your contact to send you another one."],
["unsupported link", {type: "error", errorType: {type: "unsupportedConnReq"}}, "Unsupported connection link: This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link."],
[
"name without a valid link",
{type: "error", errorType: {type: "simplexDomainNotReady", simplexDomain: domain, simplexDomainError: {type: "noValidLink"}}},
"No valid link: The SimpleX name ridgeline.simplex is registered, but it has no valid link.",
],
[
"name not in a profile",
{type: "error", errorType: {type: "simplexDomainNotReady", simplexDomain: domain, simplexDomainError: {type: "unknownDomain"}}},
"Unconfirmed name: The SimpleX name ridgeline.simplex is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner.",
],
[
"file servers not approved",
{type: "error", errorType: {type: "fileNotApproved", fileId: 7, unknownServers: ["xftp://k@xftp2.example.com", "xftp://k@xftp1.example.com:443", "xftp://j@xftp2.example.com"]}},
"Unknown servers! Without Tor or VPN, your IP address will be visible to these XFTP relays: xftp1.example.com, xftp2.example.com.",
],
["no name servers", agentError({type: "NO_NAME_SERVERS"}), "SimpleX name error: None of your servers are set to resolve SimpleX names. Use a connection link."],
["name not found", agentError({type: "SMP", serverAddress: server, smpErr: {type: "NAME", nameErr: {type: "NOT_FOUND"}}}), "Name not found: This SimpleX name is not registered. Please check the name."],
[
"no name resolver",
agentError({type: "SMP", serverAddress: server, smpErr: {type: "NAME", nameErr: {type: "NO_RESOLVER"}}}),
`SimpleX name error: Server ${server} does not support name resolution. Use a connection link.`,
],
["resolver error", agentError({type: "SMP", serverAddress: server, smpErr: {type: "NAME", nameErr: {type: "RESOLVER", resolverErr: "bad\nreply"}}}), "SimpleX name error: Resolver error: bad reply"],
[
"link removed",
agentError({type: "SMP", serverAddress: server, smpErr: {type: "AUTH"}}),
"Connection link removed: Your contact removed this link, or it was a one-time link that was already used. To connect, ask your contact to create a new link.",
],
["blocked", agentError({type: "SMP", serverAddress: server, smpErr: {type: "BLOCKED", blockInfo: {reason: T.BlockingReason.Spam}}}), "Connection blocked: Connection is blocked by server operator: Spam."],
[
"blocked content",
agentError({type: "SMP", serverAddress: server, smpErr: {type: "BLOCKED", blockInfo: {reason: T.BlockingReason.Content}}}),
"Connection blocked: Connection is blocked by server operator: Content violates conditions of use.",
],
["quota", agentError({type: "SMP", serverAddress: server, smpErr: {type: "QUOTA"}}), "Undelivered messages: The connection reached the limit of undelivered messages, your contact may be offline."],
["already connected", agentError({type: "INTERNAL", internalErr: "SEUniqueID"}), "Already connected? It seems like you are already connected via this link. If it is not the case, there was an error (SEUniqueID)."],
["timeout", agentError(broker({type: "TIMEOUT"})), "Connection timeout: Please check your network connection with smp4.simplex.im and try again."],
["network", agentError(broker({type: "NETWORK", networkError: {type: "connectError", connectError: "refused"}})), "Connection error: Please check your network connection with smp4.simplex.im and try again."],
["unknown CA", agentError(broker({type: "NETWORK", networkError: {type: "unknownCAError"}})), "Connection error: Fingerprint in server address does not match certificate: smp4.simplex.im."],
["host", agentError(broker({type: "HOST"})), "Connection error: Server address is incompatible with network settings: smp4.simplex.im."],
["server version", agentError(broker({type: "TRANSPORT", transportErr: {type: "version"}})), "Connection error: Server version is incompatible with your app: smp4.simplex.im."],
["forwarding timeout", agentError(forwarding({type: "TIMEOUT"})), "Private routing error: Error connecting to forwarding server smp4.simplex.im. Please try later."],
["forwarding network", agentError(forwarding({type: "NETWORK", networkError: {type: "failedError"}})), "Private routing error: Error connecting to forwarding server smp4.simplex.im. Please try later."],
[
"forwarding unknown CA",
agentError(forwarding({type: "NETWORK", networkError: {type: "unknownCAError"}})),
"Private routing error: Fingerprint in forwarding server address does not match certificate: smp4.simplex.im.",
],
["forwarding host", agentError(forwarding({type: "HOST"})), "Private routing error: Forwarding server address is incompatible with network settings: smp4.simplex.im."],
[
"forwarding version",
agentError(forwarding({type: "TRANSPORT", transportErr: {type: "version"}})),
"Private routing error: Forwarding server version is incompatible with network settings: smp4.simplex.im.",
],
[
"destination timeout",
agentError(destination({type: "BROKER", brokerErr: {type: "TIMEOUT"}})),
"Private routing error: Forwarding server smp4.simplex.im failed to connect to destination server smp5.simplex.im. Please try later.",
],
[
"destination network",
agentError(destination({type: "BROKER", brokerErr: {type: "NETWORK", networkError: {type: "timeoutError"}}})),
"Private routing error: Forwarding server smp4.simplex.im failed to connect to destination server smp5.simplex.im. Please try later.",
],
[
"no private routing session",
agentError(destination({type: "NO_SESSION"})),
"Private routing error: Forwarding server smp4.simplex.im failed to connect to destination server smp5.simplex.im. Please try later.",
],
[
"destination unknown CA",
agentError(destination({type: "BROKER", brokerErr: {type: "NETWORK", networkError: {type: "unknownCAError"}}})),
"Private routing error: Fingerprint in destination server address does not match certificate: smp5.simplex.im.",
],
[
"destination host",
agentError(destination({type: "BROKER", brokerErr: {type: "HOST"}})),
"Private routing error: Destination server address of smp5.simplex.im is incompatible with forwarding server smp4.simplex.im settings.",
],
[
"destination version",
agentError(destination({type: "BROKER", brokerErr: {type: "TRANSPORT", transportErr: {type: "version"}}})),
"Private routing error: Destination server version of smp5.simplex.im is incompatible with forwarding server smp4.simplex.im.",
],
["service timeout", agentError({type: "AGENT", agentErr: {type: "A_SERVICE", serviceError: {type: "timeout"}}}), "Connection timeout: The service did not respond. Please try again."],
["large message", {type: "errorStore", storeError: {type: "largeMsg"}}, "Message is too large! Please reduce the message size and send again."],
["group not found", {type: "errorStore", storeError: {type: "groupNotFound", groupId: 15}}, "Group not found! This group no longer exists."],
]
it.each(cases)("reads as the apps word it: %s", (_, chatError, text) => {
expect(chatErrorText(chatError)).toBe(text)
})
it("keeps the chain of error types and their scalar fields for other errors", () => {
expect(chatErrorText({type: "error", errorType: {type: "groupUserRole", groupInfo: {groupId: 15}, requiredRole: T.GroupMemberRole.Author}} as unknown as T.ChatError)).toBe(
'SimpleX error: error: groupUserRole: {"requiredRole":"author"}',
)
const noService = {...agentError(broker({type: "NO_SERVICE"})), connectionEntity_: {type: "rcvDirectMsgConnection", entityConnection: {connId: 1}}} as T.ChatError
expect(chatErrorText(noService)).toBe(`SimpleX error: errorAgent: BROKER: NO_SERVICE: {"agentConnId":"","brokerAddress":"${server}"}`)
expect(chatErrorText(agentError({type: "INTERNAL", internalErr: "other\nline"}))).toBe('SimpleX error: errorAgent: INTERNAL: {"agentConnId":"","internalErr":"other\\nline"}')
})
})