mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-14 06:57:51 +00:00
* 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
175 lines
8.9 KiB
TypeScript
175 lines
8.9 KiB
TypeScript
import {helpText, parseCommand, validAgentName} from "../../src/cli"
|
|
import {entryText} from "../../src/control"
|
|
import {ExitError} from "../../src/log"
|
|
import {validName} from "../../src/messages"
|
|
import {ServeOptions, registerHint} from "../../src/serve"
|
|
|
|
const home = "/home/alice"
|
|
|
|
function usage(args: string[]): string {
|
|
try {
|
|
parseCommand(args, home)
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(ExitError)
|
|
expect((error as ExitError).code).toBe(2)
|
|
return (error as ExitError).message
|
|
}
|
|
throw new Error("arguments accepted")
|
|
}
|
|
|
|
describe("serve options", () => {
|
|
it("has defaults", () => {
|
|
expect(parseCommand(["serve", "agent"], home)).toEqual({
|
|
command: "serve",
|
|
options: {
|
|
name: "agent",
|
|
data: "/home/alice/.simplex-mcp/agent",
|
|
dataGiven: false,
|
|
approve: "requests",
|
|
expireHours: 23,
|
|
resetOwners: false,
|
|
newToken: false,
|
|
port: 5226,
|
|
qrCode: "small",
|
|
stopWait: 10,
|
|
},
|
|
})
|
|
})
|
|
|
|
it("reads every option", () => {
|
|
const {options} = parseCommand(
|
|
["serve", "agent-2", "--data", "/tmp/a", "--display-name", "Agent Two", "--approve", "host", "--expire", "1", "--stop-wait", "0", "--port", "5227", "--qr-code", "large", "--new-token", "--reset-owners", "--owners"],
|
|
home,
|
|
) as {options: object}
|
|
expect(options).toEqual({
|
|
name: "agent-2",
|
|
data: "/tmp/a",
|
|
dataGiven: true,
|
|
displayName: "Agent Two",
|
|
approve: "host",
|
|
expireHours: 1,
|
|
stopWait: 0,
|
|
port: 5227,
|
|
qrCode: "large",
|
|
newToken: true,
|
|
resetOwners: true,
|
|
owners: [],
|
|
})
|
|
})
|
|
|
|
it("takes the QR code size", () => {
|
|
expect(parseCommand(["serve", "agent", "--qr-code", "small"], home)).toMatchObject({options: {qrCode: "small"}})
|
|
expect(parseCommand(["serve", "agent", "--qr-code", "large"], home)).toMatchObject({options: {qrCode: "large"}})
|
|
expect(usage(["serve", "agent", "--qr-code", "huge"])).toBe("--qr-code: small or large expected, got huge")
|
|
expect(usage(["serve", "agent", "--qr-code"])).toBe("--qr-code: a value expected.")
|
|
})
|
|
|
|
it("takes --owners with and without a list", () => {
|
|
expect(parseCommand(["serve", "agent", "--owners", "--port", "5300"], home)).toMatchObject({options: {owners: [], port: 5300}})
|
|
expect(parseCommand(["serve", "agent", "--owners", "3:Alice Smith,7:Bob"], home)).toMatchObject({
|
|
options: {owners: [{memberId: 3, name: "Alice Smith"}, {memberId: 7, name: "Bob"}]},
|
|
})
|
|
})
|
|
|
|
it("splits --owners only at commas before an entry ID, so a name may have commas", () => {
|
|
expect(parseCommand(["serve", "agent", "--owners", "3:Smith, Alice,7:Bob,,Jr,12:Lu,8"], home)).toMatchObject({
|
|
options: {owners: [{memberId: 3, name: "Smith, Alice"}, {memberId: 7, name: "Bob,,Jr"}, {memberId: 12, name: "Lu,8"}]},
|
|
})
|
|
})
|
|
|
|
it("checks ranges", () => {
|
|
expect(usage(["serve", "agent", "--expire", "24"])).toBe("--expire: hours from 1 to 23 expected, got 24")
|
|
expect(usage(["serve", "agent", "--expire", "0"])).toBe("--expire: hours from 1 to 23 expected, got 0")
|
|
expect(parseCommand(["serve", "agent", "--stop-wait", "60"], home)).toMatchObject({options: {stopWait: 60}})
|
|
expect(usage(["serve", "agent", "--stop-wait", "61"])).toBe("--stop-wait: seconds from 0 to 60 expected, got 61")
|
|
expect(usage(["serve", "agent", "--stop-wait", "-1"])).toBe("--stop-wait: seconds from 0 to 60 expected, got -1")
|
|
expect(usage(["serve", "agent", "--stop-wait", "2.5"])).toBe("--stop-wait: seconds from 0 to 60 expected, got 2.5")
|
|
expect(usage(["serve", "agent", "--stop-wait"])).toBe("--stop-wait: a value expected.")
|
|
expect(usage(["serve", "agent", "--port", "65536"])).toBe("--port: a port from 1 to 65535 expected, got 65536")
|
|
expect(usage(["serve", "agent", "--port", "x"])).toBe("--port: a port from 1 to 65535 expected, got x")
|
|
expect(usage(["serve", "agent", "--approve", "all"])).toBe("--approve: requests or host expected, got all")
|
|
expect(usage(["serve", "agent", "--port"])).toBe("--port: a value expected.")
|
|
})
|
|
|
|
it("rejects arguments", () => {
|
|
expect(usage(["serve", "agent", "--reset-owners", "--owners", "3:Alice"])).toBe("--reset-owners takes --owners without a list: member IDs change in a new agent control chat.")
|
|
expect(usage(["serve", "agent", "--owners", "Alice"])).toBe("--owners: ID:NAME expected, got Alice")
|
|
expect(usage(["serve", "Agent"])).toBe("NAME: 1 to 32 lowercase letters, digits and single hyphens, starting with a letter or digit.")
|
|
expect(usage(["serve"])).toBe("NAME: 1 to 32 lowercase letters, digits and single hyphens, starting with a letter or digit.")
|
|
expect(usage(["serve", "agent", "--colour"])).toBe("Unknown option --colour. Run simplex-mcp --help.")
|
|
expect(usage(["start", "agent"])).toMatch(/^Usage: /)
|
|
})
|
|
|
|
it("checks the display name", () => {
|
|
expect(usage(["serve", "agent", "--display-name", "agent.bot"])).toBe("--display-name: not a valid SimpleX name; try agentbot")
|
|
expect(usage(["serve", "agent", "--display-name", " agent"])).toBe("--display-name: not a valid SimpleX name; try agent")
|
|
expect(parseCommand(["serve", "agent", "--display-name", "Ridgeline Scout"], home)).toMatchObject({options: {displayName: "Ridgeline Scout"}})
|
|
})
|
|
})
|
|
|
|
describe("NAME", () => {
|
|
it("accepts lowercase letters, digits and single hyphens", () => {
|
|
for (const name of ["agent", "a", "agent-2", "2agent", "a-b-c", "a-", "x".repeat(32)]) expect(validAgentName(name)).toBe(true)
|
|
for (const name of ["", "-a", "a--b", "A", "a_b", "a.b", "x".repeat(33)]) expect(validAgentName(name)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("display names", () => {
|
|
it("follows mkValidName", () => {
|
|
expect(validName("Alice")).toBe("Alice")
|
|
expect(validName("Alice Smith")).toBe("Alice Smith")
|
|
expect(validName("Alice Smith ")).toBe("Alice Smith")
|
|
expect(validName("@alice")).toBe("alice")
|
|
expect(validName("alice_12")).toBe("alice-12")
|
|
expect(validName("a-b")).toBe("a-b")
|
|
expect(validName("x".repeat(60))).toBe("x".repeat(50))
|
|
expect(validName("$")).toBe("")
|
|
})
|
|
})
|
|
|
|
describe("register options", () => {
|
|
it("has defaults and reads options", () => {
|
|
expect(parseCommand(["register", "agent"], home)).toEqual({command: "register", options: {name: "agent", data: "/home/alice/.simplex-mcp/agent", scope: "user"}})
|
|
expect(parseCommand(["register", "agent", "--scope", "local", "--data", "/tmp/a"], home)).toEqual({command: "register", options: {name: "agent", data: "/tmp/a", scope: "local"}})
|
|
expect(usage(["register", "agent", "--scope", "project"])).toBe("--scope: user or local expected, got project")
|
|
})
|
|
})
|
|
|
|
describe("printed commands", () => {
|
|
const platform = Object.getOwnPropertyDescriptor(process, "platform")!
|
|
const onPlatform = (value: NodeJS.Platform): void => {
|
|
Object.defineProperty(process, "platform", {...platform, value})
|
|
}
|
|
const hint = (data?: string): string => registerHint({...(parseCommand(["serve", "agent"], home) as {options: ServeOptions}).options, ...(data === undefined ? {} : {data, dataGiven: true})})
|
|
|
|
afterEach(() => Object.defineProperty(process, "platform", platform))
|
|
|
|
it("quote owner entries and the data folder for a POSIX shell", () => {
|
|
onPlatform("linux")
|
|
expect(entryText({memberId: 3, name: "Alice"})).toBe("3:Alice")
|
|
expect(entryText({memberId: 3, name: "Ana-Lu_2"})).toBe("3:Ana-Lu_2")
|
|
expect(entryText({memberId: 3, name: "Alice Smith"})).toBe("'3:Alice Smith'")
|
|
expect(entryText({memberId: 3, name: "O'Neil"})).toBe("'3:O'\\''Neil'")
|
|
expect(hint()).toBe("Add the agent to Claude Code:\n npx -y @simplex-chat/mcp register agent")
|
|
expect(hint("/tmp/a-1/b_2.c")).toBe("Add the agent to Claude Code:\n npx -y @simplex-chat/mcp register agent --data /tmp/a-1/b_2.c")
|
|
expect(hint("/tmp/my agent's")).toBe("Add the agent to Claude Code:\n npx -y @simplex-chat/mcp register agent --data '/tmp/my agent'\\''s'")
|
|
})
|
|
|
|
it("quote with double quotes on Windows", () => {
|
|
onPlatform("win32")
|
|
expect(entryText({memberId: 3, name: "Ana-Lu_2"})).toBe("3:Ana-Lu_2")
|
|
expect(entryText({memberId: 3, name: "Alice Smith"})).toBe('"3:Alice Smith"')
|
|
expect(hint("C:\\Users\\alice\\agent")).toBe("Add the agent to Claude Code:\n npx -y @simplex-chat/mcp register agent --data C:\\Users\\alice\\agent")
|
|
expect(hint("C:\\Users\\Ana Lu\\agent")).toBe('Add the agent to Claude Code:\n npx -y @simplex-chat/mcp register agent --data "C:\\Users\\Ana Lu\\agent"')
|
|
})
|
|
})
|
|
|
|
describe("help", () => {
|
|
it("matches the manual", () => {
|
|
expect(parseCommand(["--help"], home)).toEqual({command: "help"})
|
|
expect(helpText).toContain(" --owners [ID:NAME,...] only these owners approve; without a list, the link\n")
|
|
expect(helpText).toContain(" --stop-wait SECONDS at stop, wait SECONDS for cancelled cards to be sent,\n 0 to 60 (default: 10)\n")
|
|
expect(helpText.endsWith("--reset-owners takes --owners without a list.\n")).toBe(true)
|
|
})
|
|
})
|