From 8eccfe63c712530793dfa4648c1a0f75813b735a Mon Sep 17 00:00:00 2001 From: konard Date: Thu, 19 Mar 2026 12:27:26 +0000 Subject: [PATCH 1/3] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/xlabtg/teleton-plugins/issues/25 --- .gitkeep | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitkeep b/.gitkeep index c46a5b9..a480968 100644 --- a/.gitkeep +++ b/.gitkeep @@ -1 +1,2 @@ -# .gitkeep file auto-generated at 2026-03-19T10:37:13.073Z for PR creation at branch issue-19-f54b585823d1 for issue https://github.com/xlabtg/teleton-plugins/issues/19 \ No newline at end of file +# .gitkeep file auto-generated at 2026-03-19T10:37:13.073Z for PR creation at branch issue-19-f54b585823d1 for issue https://github.com/xlabtg/teleton-plugins/issues/19 +# Updated: 2026-03-19T12:27:26.485Z \ No newline at end of file From d33e44b68c638a71bf10f0d5450707790e400da9 Mon Sep 17 00:00:00 2001 From: konard Date: Thu, 19 Mar 2026 12:31:49 +0000 Subject: [PATCH 2/3] fix(ton-bridge): remove empty package.json/lock to prevent npm install failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ton-bridge plugin had package.json and package-lock.json files but declared no npm dependencies. The PluginLoader in teleton-agent runs `npm ci` for any plugin directory that contains both files, causing `Error: spawn npm ENOENT` in environments where npm is not in PATH (e.g. the agent's production runtime). This prevented the plugin from being marked as available, resulting in the "TON Bridge недоступен — модуль не запущен" message. Plugins with no external dependencies should not have package.json — consistent with all other zero-dependency plugins in the repo (boards, casino, crypto-prices, dyor, example, fragment, etc.). Also adds 25 unit tests covering manifest exports, all three tools (ton_bridge_open, ton_bridge_about, ton_bridge_custom_message), buttonText fallback logic, startParam URL building, and error handling. Co-Authored-By: Claude Sonnet 4.6 --- plugins/ton-bridge/package-lock.json | 13 - plugins/ton-bridge/package.json | 16 -- plugins/ton-bridge/tests/index.test.js | 333 +++++++++++++++++++++++++ 3 files changed, 333 insertions(+), 29 deletions(-) delete mode 100644 plugins/ton-bridge/package-lock.json delete mode 100644 plugins/ton-bridge/package.json create mode 100644 plugins/ton-bridge/tests/index.test.js diff --git a/plugins/ton-bridge/package-lock.json b/plugins/ton-bridge/package-lock.json deleted file mode 100644 index 5c23e33..0000000 --- a/plugins/ton-bridge/package-lock.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "ton-bridge", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "ton-bridge", - "version": "1.0.0", - "license": "MIT" - } - } -} diff --git a/plugins/ton-bridge/package.json b/plugins/ton-bridge/package.json deleted file mode 100644 index dbaab97..0000000 --- a/plugins/ton-bridge/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "ton-bridge", - "version": "1.0.0", - "description": "Share TON Bridge Mini App link with a button. Opens https://t.me/TONBridge_robot?startapp", - "author": "xlabtg (https://github.com/xlabtg)", - "type": "module", - "main": "index.js", - "scripts": {}, - "keywords": [ - "ton", - "bridge", - "miniapp", - "tonbridge" - ], - "license": "MIT" -} diff --git a/plugins/ton-bridge/tests/index.test.js b/plugins/ton-bridge/tests/index.test.js new file mode 100644 index 0000000..929ae40 --- /dev/null +++ b/plugins/ton-bridge/tests/index.test.js @@ -0,0 +1,333 @@ +/** + * Unit tests for ton-bridge plugin + * + * Tests manifest exports, tool definitions, and tool execute behavior + * using Node's built-in test runner (node:test). + */ + +import { describe, it, before } from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import { pathToFileURL } from "node:url"; +import { resolve, join } from "node:path"; + +const PLUGIN_DIR = resolve("plugins/ton-bridge"); +const PLUGIN_URL = pathToFileURL(join(PLUGIN_DIR, "index.js")).href; + +// ─── Minimal mock SDK ──────────────────────────────────────────────────────── + +function makeSdk(overrides = {}) { + return { + pluginConfig: { + buttonText: "TON Bridge No1", + startParam: "", + }, + log: { + info: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {}, + }, + telegram: { + sendMessage: async () => 42, + ...overrides.telegram, + }, + ...overrides, + }; +} + +function makeContext(overrides = {}) { + return { + chatId: 123456789, + senderId: 987654321, + ...overrides, + }; +} + +// ─── Load plugin once ───────────────────────────────────────────────────────── + +let mod; + +before(async () => { + mod = await import(PLUGIN_URL); +}); + +// ─── Tests ──────────────────────────────────────────────────────────────────── + +describe("ton-bridge plugin", () => { + describe("manifest", () => { + it("exports manifest object", () => { + assert.ok(mod.manifest, "manifest should be exported"); + assert.equal(typeof mod.manifest, "object"); + }); + + it("manifest has required name field", () => { + assert.equal(mod.manifest.name, "ton-bridge"); + }); + + it("manifest has version", () => { + assert.ok(mod.manifest.version, "manifest.version should exist"); + }); + + it("manifest has sdkVersion", () => { + assert.ok(mod.manifest.sdkVersion, "manifest.sdkVersion should exist"); + }); + + it("manifest has defaultConfig with buttonText", () => { + assert.ok(mod.manifest.defaultConfig, "defaultConfig should exist"); + assert.ok(mod.manifest.defaultConfig.buttonText, "defaultConfig.buttonText should exist"); + }); + }); + + describe("tools export", () => { + it("exports tools as a function", () => { + assert.equal(typeof mod.tools, "function", "tools should be a function"); + }); + + it("tools(sdk) returns an array", () => { + const sdk = makeSdk(); + const toolList = mod.tools(sdk); + assert.ok(Array.isArray(toolList), "tools(sdk) should return an array"); + }); + + it("returns 3 tools", () => { + const sdk = makeSdk(); + const toolList = mod.tools(sdk); + assert.equal(toolList.length, 3, "should have 3 tools"); + }); + + it("all tools have required fields: name, description, execute", () => { + const sdk = makeSdk(); + const toolList = mod.tools(sdk); + for (const tool of toolList) { + assert.ok(tool.name, `tool.name must exist (got: ${JSON.stringify(tool.name)})`); + assert.ok(tool.description, `tool "${tool.name}" must have description`); + assert.equal(typeof tool.execute, "function", `tool "${tool.name}" must have execute function`); + } + }); + + it("tool names match expected set", () => { + const sdk = makeSdk(); + const names = mod.tools(sdk).map((t) => t.name); + assert.ok(names.includes("ton_bridge_open"), "should have ton_bridge_open"); + assert.ok(names.includes("ton_bridge_about"), "should have ton_bridge_about"); + assert.ok(names.includes("ton_bridge_custom_message"), "should have ton_bridge_custom_message"); + }); + }); + + describe("ton_bridge_open", () => { + it("returns success when sendMessage succeeds", async () => { + let capturedChatId, capturedText, capturedOpts; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedChatId = chatId; + capturedText = text; + capturedOpts = opts; + return 55; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + const result = await tool.execute({}, makeContext({ chatId: 111 })); + + assert.equal(result.success, true); + assert.equal(result.data.message_id, 55); + assert.equal(result.data.chat_id, 111); + assert.equal(capturedChatId, 111); + assert.ok(capturedText, "message text should be provided"); + assert.ok(capturedOpts.inlineKeyboard, "inline keyboard should be included"); + }); + + it("uses custom message when provided", async () => { + let capturedText; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text) => { + capturedText = text; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({ message: "Custom text" }, makeContext()); + assert.equal(capturedText, "Custom text"); + }); + + it("uses custom buttonText when provided", async () => { + let capturedOpts; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({ buttonText: "Open Bridge" }, makeContext()); + assert.equal(capturedOpts.inlineKeyboard[0][0].text, "Open Bridge"); + }); + + it("falls back to sdk.pluginConfig.buttonText when no buttonText param", async () => { + let capturedOpts; + const sdk = makeSdk({ + pluginConfig: { buttonText: "My Bridge Button", startParam: "" }, + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({}, makeContext()); + assert.equal(capturedOpts.inlineKeyboard[0][0].text, "My Bridge Button"); + }); + + it("button URL points to TON Bridge", async () => { + let capturedOpts; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({}, makeContext()); + const url = capturedOpts.inlineKeyboard[0][0].url; + assert.ok(url.includes("TONBridge_robot"), `URL should reference TONBridge_robot, got: ${url}`); + }); + + it("returns failure when sendMessage throws", async () => { + const sdk = makeSdk({ + telegram: { + sendMessage: async () => { throw new Error("Telegram error"); }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + const result = await tool.execute({}, makeContext()); + assert.equal(result.success, false); + assert.ok(result.error); + }); + }); + + describe("ton_bridge_about", () => { + it("returns success when sendMessage succeeds", async () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_about"); + const result = await tool.execute({}, makeContext()); + assert.equal(result.success, true); + assert.ok(result.data.message_id != null); + }); + + it("message contains TON Bridge info", async () => { + let capturedText; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text) => { + capturedText = text; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_about"); + await tool.execute({}, makeContext()); + assert.ok(capturedText.toLowerCase().includes("bridge"), "about message should mention bridge"); + }); + + it("returns failure when sendMessage throws", async () => { + const sdk = makeSdk({ + telegram: { + sendMessage: async () => { throw new Error("network error"); }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_about"); + const result = await tool.execute({}, makeContext()); + assert.equal(result.success, false); + }); + }); + + describe("ton_bridge_custom_message", () => { + it("sends customMessage as text", async () => { + let capturedText; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text) => { + capturedText = text; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_custom_message"); + await tool.execute({ customMessage: "Hello TON!" }, makeContext()); + assert.equal(capturedText, "Hello TON!"); + }); + + it("returns success with message_id and chat_id", async () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_custom_message"); + const result = await tool.execute({ customMessage: "Bridge now" }, makeContext({ chatId: 999 })); + assert.equal(result.success, true); + assert.equal(result.data.chat_id, 999); + assert.equal(result.data.message_id, 42); + }); + + it("returns failure when sendMessage throws", async () => { + const sdk = makeSdk({ + telegram: { + sendMessage: async () => { throw new Error("flood"); }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_custom_message"); + const result = await tool.execute({ customMessage: "test" }, makeContext()); + assert.equal(result.success, false); + assert.ok(result.error); + }); + + it("uses customMessage parameter as required", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_custom_message"); + assert.ok(tool.parameters?.required?.includes("customMessage"), "customMessage should be required"); + }); + }); + + describe("startParam URL building", () => { + it("appends startParam to URL when set", async () => { + let capturedOpts; + const sdk = makeSdk({ + pluginConfig: { buttonText: "Bridge", startParam: "myref" }, + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({}, makeContext()); + const url = capturedOpts.inlineKeyboard[0][0].url; + assert.ok(url.includes("myref"), `URL should include startParam, got: ${url}`); + }); + + it("does not append startParam when empty", async () => { + let capturedOpts; + const sdk = makeSdk({ + pluginConfig: { buttonText: "Bridge", startParam: "" }, + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({}, makeContext()); + const url = capturedOpts.inlineKeyboard[0][0].url; + // URL should be the base URL without extra params appended via = + assert.ok(url.endsWith("startapp"), `URL without startParam should end with 'startapp', got: ${url}`); + }); + }); +}); From e022aeac7dce12c9a7522651c8b682024d88806a Mon Sep 17 00:00:00 2001 From: konard Date: Thu, 19 Mar 2026 12:32:29 +0000 Subject: [PATCH 3/3] Revert "Initial commit with task details" This reverts commit 8eccfe63c712530793dfa4648c1a0f75813b735a. --- .gitkeep | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitkeep b/.gitkeep index a480968..c46a5b9 100644 --- a/.gitkeep +++ b/.gitkeep @@ -1,2 +1 @@ -# .gitkeep file auto-generated at 2026-03-19T10:37:13.073Z for PR creation at branch issue-19-f54b585823d1 for issue https://github.com/xlabtg/teleton-plugins/issues/19 -# Updated: 2026-03-19T12:27:26.485Z \ No newline at end of file +# .gitkeep file auto-generated at 2026-03-19T10:37:13.073Z for PR creation at branch issue-19-f54b585823d1 for issue https://github.com/xlabtg/teleton-plugins/issues/19 \ No newline at end of file