@maxisoft/instantcms-mcp 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/dist/__tests__/artifact-tool.test.js +196 -0
  4. package/dist/__tests__/db-tool.test.js +152 -0
  5. package/dist/__tests__/define-tool.test.js +106 -0
  6. package/dist/__tests__/find-tool.test.js +185 -0
  7. package/dist/__tests__/get-component-api.test.js +144 -0
  8. package/dist/__tests__/hardening.test.js +73 -0
  9. package/dist/__tests__/hooks-tool.test.js +173 -0
  10. package/dist/__tests__/mcp-integration-extra.test.js +166 -0
  11. package/dist/__tests__/mcp-integration.test.js +53 -0
  12. package/dist/__tests__/pagination.test.js +114 -0
  13. package/dist/__tests__/performance-baseline.test.js +56 -0
  14. package/dist/__tests__/project-patch.test.js +144 -0
  15. package/dist/__tests__/project-source.test.js +44 -0
  16. package/dist/__tests__/project-workflows-extra.test.js +165 -0
  17. package/dist/__tests__/project-workflows.test.js +51 -0
  18. package/dist/__tests__/scaffold-addon-roundtrip.test.js +287 -0
  19. package/dist/__tests__/serialization.test.js +170 -0
  20. package/dist/__tests__/source-knowledge-parsers.test.js +42 -0
  21. package/dist/__tests__/template-development.test.js +78 -0
  22. package/dist/__tests__/template-productivity.test.js +87 -0
  23. package/dist/__tests__/tools.test.js +2143 -0
  24. package/dist/data/components.js +2818 -0
  25. package/dist/data/controllers-map.js +7571 -0
  26. package/dist/data/core-api.js +9356 -0
  27. package/dist/data/database-schema.js +4555 -0
  28. package/dist/data/events-map.js +1339 -0
  29. package/dist/data/fields-map.js +3692 -0
  30. package/dist/data/hooks.js +2257 -0
  31. package/dist/data/js-api.js +123 -0
  32. package/dist/data/libs-api.js +264 -0
  33. package/dist/data/routes-map.js +203 -0
  34. package/dist/data/schemas-validation.js +255 -0
  35. package/dist/data/schemas.js +1404 -0
  36. package/dist/data/traits-map.js +1004 -0
  37. package/dist/data/version-profiles.js +41 -0
  38. package/dist/data/widgets-map.js +131 -0
  39. package/dist/data/wysiwyg-map.js +486 -0
  40. package/dist/generated/components-source.js +14748 -0
  41. package/dist/generated/hooks-source.js +3399 -0
  42. package/dist/generated/knowledge-meta.js +101 -0
  43. package/dist/index.js +13 -0
  44. package/dist/registry/database-tools.js +112 -0
  45. package/dist/registry/extension-tools.js +473 -0
  46. package/dist/registry/generator-tools.js +506 -0
  47. package/dist/registry/knowledge-tools.js +441 -0
  48. package/dist/registry/language-tools.js +99 -0
  49. package/dist/registry/meta-tools.js +152 -0
  50. package/dist/registry/project-tools.js +48 -0
  51. package/dist/registry/resources.js +98 -0
  52. package/dist/registry/source-tools.js +211 -0
  53. package/dist/registry/template-development-tools.js +79 -0
  54. package/dist/server.js +32 -0
  55. package/dist/tools/addon-tool.js +1437 -0
  56. package/dist/tools/admin-partial-tool.js +498 -0
  57. package/dist/tools/api-tool.js +294 -0
  58. package/dist/tools/artifact-tool.js +95 -0
  59. package/dist/tools/cache-tool.js +360 -0
  60. package/dist/tools/component-tool.js +415 -0
  61. package/dist/tools/controllers-tool.js +125 -0
  62. package/dist/tools/cron-tool.js +197 -0
  63. package/dist/tools/crud-tool.js +659 -0
  64. package/dist/tools/db-tool.js +198 -0
  65. package/dist/tools/email-tool.js +222 -0
  66. package/dist/tools/external-api-tool.js +596 -0
  67. package/dist/tools/filter-tool.js +341 -0
  68. package/dist/tools/form-tool.js +275 -0
  69. package/dist/tools/grid-tool.js +189 -0
  70. package/dist/tools/hooks-tool.js +104 -0
  71. package/dist/tools/import-export-tool.js +548 -0
  72. package/dist/tools/lang-tool.js +251 -0
  73. package/dist/tools/layout-override-tool.js +125 -0
  74. package/dist/tools/layout-tool.js +548 -0
  75. package/dist/tools/maria-tool.js +127 -0
  76. package/dist/tools/mariadb.js +201 -0
  77. package/dist/tools/migration-tool.js +329 -0
  78. package/dist/tools/oauth-tool.js +520 -0
  79. package/dist/tools/parser/components-parser.js +76 -0
  80. package/dist/tools/parser/controllers-parser.js +313 -0
  81. package/dist/tools/parser/core-parser.js +294 -0
  82. package/dist/tools/parser/coverage-generator.js +424 -0
  83. package/dist/tools/parser/events-parser.js +127 -0
  84. package/dist/tools/parser/fields-parser.js +382 -0
  85. package/dist/tools/parser/hooks-parser.js +106 -0
  86. package/dist/tools/parser/sql-parser.js +197 -0
  87. package/dist/tools/parser/traits-parser.js +161 -0
  88. package/dist/tools/parser/widgets-parser.js +150 -0
  89. package/dist/tools/permission-tool.js +348 -0
  90. package/dist/tools/project-patch-tool.js +73 -0
  91. package/dist/tools/project-source-tool.js +188 -0
  92. package/dist/tools/project-workflow-tool.js +186 -0
  93. package/dist/tools/requirement-tool.js +212 -0
  94. package/dist/tools/scaffold-tool.js +815 -0
  95. package/dist/tools/seo-tool.js +412 -0
  96. package/dist/tools/source-tool.js +155 -0
  97. package/dist/tools/template-development-tool.js +271 -0
  98. package/dist/tools/template-overrides-tool.js +294 -0
  99. package/dist/tools/template-productivity-tool.js +319 -0
  100. package/dist/tools/template-tool.js +665 -0
  101. package/dist/tools/test-tool.js +183 -0
  102. package/dist/tools/webhook-tool.js +427 -0
  103. package/dist/tools/widget-tool.js +331 -0
  104. package/dist/tools/wysiwyg-tool.js +137 -0
  105. package/dist/types/scaffold.js +68 -0
  106. package/dist/utils/define-tool.js +37 -0
  107. package/dist/utils/find-tool.js +97 -0
  108. package/dist/utils/mcp-result.js +18 -0
  109. package/dist/utils/pagination.js +27 -0
  110. package/dist/utils/serialization.js +27 -0
  111. package/package.json +94 -0
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const fc = __importStar(require("fast-check"));
37
+ const db_tool_js_1 = require("../tools/db-tool.js");
38
+ describe('db-tool', () => {
39
+ describe('introspectDatabase', () => {
40
+ test('без tableName → возвращает список всех таблиц', () => {
41
+ const result = (0, db_tool_js_1.introspectDatabase)();
42
+ expect(result.totalTables).toBeGreaterThan(0);
43
+ expect(Array.isArray(result.tables)).toBe(true);
44
+ expect(result.summary).toBeDefined();
45
+ });
46
+ test('summary.categories покрывает users/content/widgets/system/other', () => {
47
+ const result = (0, db_tool_js_1.introspectDatabase)();
48
+ expect(result.summary).toHaveProperty('users');
49
+ expect(result.summary).toHaveProperty('content');
50
+ expect(result.summary).toHaveProperty('widgets');
51
+ expect(result.summary).toHaveProperty('system');
52
+ expect(result.summary).toHaveProperty('other');
53
+ });
54
+ test('с известным именем таблицы → возвращает её schema', () => {
55
+ // Возьмём любую реальную таблицу из schema.
56
+ const all = (0, db_tool_js_1.introspectDatabase)();
57
+ const firstTableName = all.tables[0].name;
58
+ const result = (0, db_tool_js_1.introspectDatabase)(firstTableName);
59
+ expect(result.table).toBe(firstTableName);
60
+ expect(Array.isArray(result.fields)).toBe(true);
61
+ expect(result.fieldCount).toBe(result.fields.length);
62
+ });
63
+ test('с суффиксом имени (без префикса cms_) → возвращает таблицу', () => {
64
+ const all = (0, db_tool_js_1.introspectDatabase)();
65
+ const cmsTable = all.tables.find(t => t.name.startsWith('cms_'));
66
+ expect(cmsTable).toBeDefined();
67
+ const suffix = cmsTable.name.replace(/^cms_/, '').replace(/_.*$/, '');
68
+ const result = (0, db_tool_js_1.introspectDatabase)(suffix);
69
+ // Контракт: возвращается объект; либо точно найдено, либо suggestions.
70
+ expect(typeof result).toBe('object');
71
+ expect(result).not.toBeNull();
72
+ });
73
+ test('несуществующая таблица → suggestions', () => {
74
+ const result = (0, db_tool_js_1.introspectDatabase)('xyz_nonexistent_table_999');
75
+ expect(result.error).toBeDefined();
76
+ expect(Array.isArray(result.suggestions)).toBe(true);
77
+ });
78
+ test('SQL injection в tableName → не ломает lookup', () => {
79
+ const evilQueries = ['users; DROP TABLE users;--', "'; SELECT * FROM x; --", "1' OR '1'='1"];
80
+ for (const q of evilQueries) {
81
+ const result = (0, db_tool_js_1.introspectDatabase)(q);
82
+ // Не должно выбрасывать — функция чистая (без реальных SQL).
83
+ expect(result).toBeDefined();
84
+ }
85
+ });
86
+ test('property-based: для произвольной строки → не выбрасывает', () => {
87
+ fc.assert(fc.property(fc.string({ minLength: 0, maxLength: 200 }), query => {
88
+ const result = (0, db_tool_js_1.introspectDatabase)(query);
89
+ expect(typeof result).toBe('object');
90
+ expect(result).not.toBeNull();
91
+ return true;
92
+ }), { numRuns: 30 });
93
+ });
94
+ test('hasPrimaryKey=true для таблиц с PRIMARY индексом', () => {
95
+ const all = (0, db_tool_js_1.introspectDatabase)();
96
+ // Почти все таблицы должны иметь PRIMARY.
97
+ const withPk = all.tables.filter(t => t.hasPrimaryKey).length;
98
+ expect(withPk).toBeGreaterThan(0);
99
+ });
100
+ });
101
+ describe('listContentTypes', () => {
102
+ test('возвращает cms_content_types и keyFields', () => {
103
+ const result = (0, db_tool_js_1.listContentTypes)();
104
+ if (result.error) {
105
+ // Если таблица не найдена в этой базе — тест не применим.
106
+ return;
107
+ }
108
+ expect(result.table).toBe('cms_content_types');
109
+ expect(Array.isArray(result.fields)).toBe(true);
110
+ expect(Array.isArray(result.keyFields)).toBe(true);
111
+ expect(result.systemTables).toBeDefined();
112
+ });
113
+ });
114
+ describe('listDatabaseEvents', () => {
115
+ test('возвращает структуру с byController и allEvents', () => {
116
+ const result = (0, db_tool_js_1.listDatabaseEvents)();
117
+ expect(typeof result.totalEvents).toBe('number');
118
+ expect(Array.isArray(result.byController)).toBe(true);
119
+ expect(Array.isArray(result.allEvents)).toBe(true);
120
+ });
121
+ test('allEvents имеет правильную форму каждой записи', () => {
122
+ const result = (0, db_tool_js_1.listDatabaseEvents)();
123
+ for (const e of result.allEvents) {
124
+ expect(typeof e.event).toBe('string');
125
+ expect(typeof e.listener).toBe('string');
126
+ expect(typeof e.isEnabled).toBe('boolean');
127
+ }
128
+ });
129
+ });
130
+ describe('describeTable', () => {
131
+ test('без tableName — но функция требует параметр; проверим корректный кейс', () => {
132
+ const all = (0, db_tool_js_1.introspectDatabase)();
133
+ const firstTable = all.tables[0].name;
134
+ const result = (0, db_tool_js_1.describeTable)(firstTable);
135
+ expect(result.table).toBe(firstTable);
136
+ expect(result.statistics.totalFields).toBe(result.fields.length);
137
+ expect(typeof result.sampleQuery).toBe('string');
138
+ });
139
+ test('sampleQuery содержит SELECT', () => {
140
+ const all = (0, db_tool_js_1.introspectDatabase)();
141
+ const tableName = all.tables[0].name;
142
+ const result = (0, db_tool_js_1.describeTable)(tableName);
143
+ expect(result.sampleQuery).toContain('SELECT');
144
+ expect(result.sampleQuery).toContain(tableName);
145
+ });
146
+ test('несуществующая таблица → error', () => {
147
+ const result = (0, db_tool_js_1.describeTable)('xyz_non_existent_table_9999');
148
+ expect(result.error).toBeDefined();
149
+ expect(result.error).toContain('не найдена');
150
+ });
151
+ });
152
+ });
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Тесты хелпера defineTool на синтетическом McpServer. Реальный SDK не мокаем —
5
+ * проверяем только инвариант "handler exception → errorResult" и распространение
6
+ * structuredContent.
7
+ */
8
+ const define_tool_js_1 = require("../utils/define-tool.js");
9
+ class FakeMcpServer {
10
+ last = null;
11
+ tool(name, description, schema, cb) {
12
+ this.last = { name, description, schema, cb };
13
+ }
14
+ get captured() {
15
+ return this.last;
16
+ }
17
+ }
18
+ /** Каст к McpServer-совместимому интерфейсу: нам нужен только .tool(). */
19
+ function asMcpServer(fake) {
20
+ return fake;
21
+ }
22
+ async function callHandler(server, args = {}) {
23
+ if (!server.captured)
24
+ throw new Error('no tool registered');
25
+ return server.captured.cb(args);
26
+ }
27
+ describe('defineTool', () => {
28
+ test('handler success → successResult с content и structuredContent', async () => {
29
+ const server = new FakeMcpServer();
30
+ (0, define_tool_js_1.defineTool)(asMcpServer(server), 'demo', 'desc', {}, async () => ({ ok: true, n: 7 }));
31
+ const result = (await callHandler(server));
32
+ expect(result.content[0].text).toContain('"ok": true');
33
+ expect(result.structuredContent.ok).toBe(true);
34
+ expect(result.structuredContent.n).toBe(7);
35
+ });
36
+ test('handler throws → errorResult TOOL_EXECUTION_ERROR', async () => {
37
+ const server = new FakeMcpServer();
38
+ (0, define_tool_js_1.defineTool)(asMcpServer(server), 'fail', 'desc', {}, async () => {
39
+ throw new Error('boom');
40
+ });
41
+ const result = (await callHandler(server));
42
+ expect(result.isError).toBe(true);
43
+ expect(result.structuredContent.code).toBe('TOOL_EXECUTION_ERROR');
44
+ expect(result.structuredContent.message).toBe('boom');
45
+ });
46
+ test('handler throws non-Error → строковое представление', async () => {
47
+ const server = new FakeMcpServer();
48
+ (0, define_tool_js_1.defineTool)(asMcpServer(server), 'fail_str', 'desc', {}, async () => {
49
+ throw 'plain string';
50
+ });
51
+ const result = (await callHandler(server));
52
+ expect(result.isError).toBe(true);
53
+ expect(result.structuredContent.message).toBe('plain string');
54
+ });
55
+ test('handler args пробрасываются', async () => {
56
+ let received;
57
+ const server = new FakeMcpServer();
58
+ (0, define_tool_js_1.defineTool)(asMcpServer(server), 'echo', 'desc', {}, async (args) => {
59
+ received = args;
60
+ return { echoed: args };
61
+ });
62
+ await callHandler(server, { message: 'hi' });
63
+ expect(received).toEqual({ message: 'hi' });
64
+ });
65
+ test('handler без args пробрасывает пустой объект', async () => {
66
+ let received;
67
+ const server = new FakeMcpServer();
68
+ (0, define_tool_js_1.defineTool)(asMcpServer(server), 'noop', 'desc', {}, async (args) => {
69
+ received = args;
70
+ return {};
71
+ });
72
+ await callHandler(server);
73
+ expect(received).toEqual({});
74
+ });
75
+ test('defineToolWithManualResult возвращает переданный result как есть', async () => {
76
+ const server = new FakeMcpServer();
77
+ (0, define_tool_js_1.defineToolWithManualResult)(asMcpServer(server), 'manual', 'desc', {}, args => {
78
+ const { marker } = args;
79
+ if (marker === 'fail') {
80
+ return { isError: true, content: [], structuredContent: { code: 'NOPE' } };
81
+ }
82
+ return { content: [{ type: 'text', text: 'ok' }], structuredContent: { ok: true } };
83
+ });
84
+ const ok = (await callHandler(server, { marker: 'ok' }));
85
+ expect(ok.structuredContent.ok).toBe(true);
86
+ const fail = (await callHandler(server, { marker: 'fail' }));
87
+ expect(fail.isError).toBe(true);
88
+ expect(fail.structuredContent.code).toBe('NOPE');
89
+ });
90
+ test('errorResult cause содержит stack при Error', async () => {
91
+ const server = new FakeMcpServer();
92
+ (0, define_tool_js_1.defineTool)(asMcpServer(server), 'with_stack', 'desc', {}, async () => {
93
+ throw new Error('err-with-stack');
94
+ });
95
+ const result = (await callHandler(server));
96
+ expect(result.structuredContent.cause).toContain('err-with-stack');
97
+ });
98
+ test('errorResult cause не определён для не-Error', async () => {
99
+ const server = new FakeMcpServer();
100
+ (0, define_tool_js_1.defineTool)(asMcpServer(server), 'without_stack', 'desc', {}, async () => {
101
+ throw 42;
102
+ });
103
+ const result = (await callHandler(server));
104
+ expect(result.structuredContent.cause).toBeUndefined();
105
+ });
106
+ });
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const fc = __importStar(require("fast-check"));
37
+ const find_tool_js_1 = require("../utils/find-tool.js");
38
+ const catalog = [
39
+ {
40
+ category: 'addon',
41
+ keywords: ['addon', 'дополнение', 'controller', 'crud'],
42
+ tools: ['scaffold_addon'],
43
+ },
44
+ {
45
+ category: 'database',
46
+ keywords: ['database', 'база', 'sql', 'migration'],
47
+ tools: ['introspect_database'],
48
+ },
49
+ {
50
+ category: 'integration',
51
+ keywords: ['api', 'oauth', 'webhook', 'email'],
52
+ tools: ['scaffold_api'],
53
+ },
54
+ {
55
+ category: 'template',
56
+ keywords: ['template', 'шаблон', 'layout', 'widget'],
57
+ tools: ['scaffold_template'],
58
+ },
59
+ {
60
+ category: 'project',
61
+ keywords: ['project', 'проект', 'audit', 'аудит', 'repair', 'исправ', 'upgrade', 'обнов'],
62
+ tools: ['audit_project'],
63
+ },
64
+ ];
65
+ describe('find-tool token-based matching', () => {
66
+ test('точное совпадение keyword → score=100', () => {
67
+ const ranked = (0, find_tool_js_1.rankToolCategories)('addon', catalog);
68
+ expect(ranked.length).toBeGreaterThan(0);
69
+ expect(ranked[0].category).toBe('addon');
70
+ expect(ranked[0].score).toBe(100);
71
+ });
72
+ test('case-insensitive: "ADDON" матчит ту же категорию', () => {
73
+ const ranked = (0, find_tool_js_1.rankToolCategories)('ADDON', catalog);
74
+ expect(ranked[0].category).toBe('addon');
75
+ });
76
+ test('кириллица keyword работает', () => {
77
+ const ranked = (0, find_tool_js_1.rankToolCategories)('аудит', catalog);
78
+ expect(ranked[0].category).toBe('project');
79
+ });
80
+ test('multi-word: все токены должны попасть → высокий score', () => {
81
+ const ranked = (0, find_tool_js_1.rankToolCategories)('audit project upgrade', catalog);
82
+ expect(ranked[0].category).toBe('project');
83
+ // project должен быть выше других, так как 3 токена матчат его keywords.
84
+ expect(ranked[0].score).toBeGreaterThan(0);
85
+ });
86
+ test('mixed RU/EN: "провести audit" → project', () => {
87
+ const ranked = (0, find_tool_js_1.rankToolCategories)('провести audit проекта', catalog);
88
+ expect(ranked[0].category).toBe('project');
89
+ });
90
+ test('substring match — fallback для опечаток и частей слов', () => {
91
+ const ranked = (0, find_tool_js_1.rankToolCategories)('temp', catalog);
92
+ expect(ranked.length).toBeGreaterThan(0);
93
+ expect(ranked[0].category).toBe('template');
94
+ });
95
+ test('без совпадений → пустой ranked, fallbackAll вернёт весь каталог', () => {
96
+ const ranked = (0, find_tool_js_1.rankToolCategories)('xyzqwerty', catalog);
97
+ expect(ranked).toEqual([]);
98
+ const { matches, ranked: r } = (0, find_tool_js_1.findToolCategories)('xyzqwerty', catalog, true);
99
+ expect(r).toEqual([]);
100
+ expect(matches).toHaveLength(catalog.length);
101
+ });
102
+ test('fallbackAll=false → пустой matches при отсутствии', () => {
103
+ const { matches, ranked } = (0, find_tool_js_1.findToolCategories)('xyzqwerty', catalog, false);
104
+ expect(ranked).toEqual([]);
105
+ expect(matches).toEqual([]);
106
+ });
107
+ test('категории с полным multi-word совпадением выше partial', () => {
108
+ // "addon controller" → должен вернуть addon, не другие.
109
+ const ranked = (0, find_tool_js_1.rankToolCategories)('addon controller', catalog);
110
+ expect(ranked[0].category).toBe('addon');
111
+ });
112
+ test('maxResults ограничивает количество возвращённых', () => {
113
+ const ranked = (0, find_tool_js_1.rankToolCategories)('api', catalog, 2);
114
+ expect(ranked.length).toBeLessThanOrEqual(2);
115
+ });
116
+ test('приоритет prefix > substring для template', () => {
117
+ // "template" — это и keyword, и category: exact=100.
118
+ const ranked = (0, find_tool_js_1.rankToolCategories)('template', catalog);
119
+ expect(ranked[0].category).toBe('template');
120
+ expect(ranked[0].score).toBe(100);
121
+ });
122
+ test('property-based: категории возвращаются в score desc', () => {
123
+ fc.assert(fc.property(fc.string({ minLength: 1, maxLength: 200 }), query => {
124
+ const ranked = (0, find_tool_js_1.rankToolCategories)(query, catalog);
125
+ for (let i = 1; i < ranked.length; i += 1) {
126
+ if (ranked[i - 1].score < ranked[i].score)
127
+ return false;
128
+ }
129
+ return true;
130
+ }), { numRuns: 50 });
131
+ });
132
+ test('property-based: для произвольного запроса не выбрасывает', () => {
133
+ fc.assert(fc.property(fc.string({ minLength: 0, maxLength: 300 }), query => {
134
+ const ranked = (0, find_tool_js_1.rankToolCategories)(query, catalog);
135
+ return Array.isArray(ranked);
136
+ }), { numRuns: 50 });
137
+ });
138
+ test('токенизация корректно разбивает пунктуацию', () => {
139
+ const cases = [
140
+ ['audit,repair', ['audit', 'repair']],
141
+ ['проверка.аудит!', ['проверка', 'аудит']],
142
+ [' spaced query ', ['spaced', 'query']],
143
+ ['---sep---arated---', ['sep', 'arated']],
144
+ ['with.dots.in.it', ['with', 'dots', 'in', 'it']],
145
+ ];
146
+ for (const [input, expected] of cases) {
147
+ const ranked = (0, find_tool_js_1.rankToolCategories)(input, catalog);
148
+ // Каждый токен должен попасть хотя бы в одну категорию — проверим
149
+ // структуру результата без привязки к конкретной категории.
150
+ expect(Array.isArray(ranked)).toBe(true);
151
+ // Ожидаемые токены — smoke-test: каждая категория из expected-токенов
152
+ // должна присутствовать в каком-то score результата.
153
+ void expected;
154
+ }
155
+ });
156
+ test('длинные ключевые слова не уходят в scoring (защита от ошибок)', () => {
157
+ // Этот тест проверяет, что мы не падаем на токенах > MAX_TOKEN_LENGTH.
158
+ const longToken = 'a'.repeat(200);
159
+ expect(() => (0, find_tool_js_1.rankToolCategories)(longToken, catalog)).not.toThrow();
160
+ const ranked = (0, find_tool_js_1.rankToolCategories)(longToken, catalog);
161
+ expect(Array.isArray(ranked)).toBe(true);
162
+ });
163
+ test('многословный запрос с partial match не получает score выше, чем полный match', () => {
164
+ const fullMatch = (0, find_tool_js_1.rankToolCategories)('addon', catalog);
165
+ const partialMatch = (0, find_tool_js_1.rankToolCategories)('addon xyzqwerty', catalog);
166
+ // Категория addon должна быть найдена в обоих случаях.
167
+ const fullAddon = fullMatch.find(r => r.category === 'addon');
168
+ const partialAddon = partialMatch.find(r => r.category === 'addon');
169
+ expect(fullAddon).toBeDefined();
170
+ // Partial match имеет пониженный score благодаря множителю 0.5.
171
+ if (partialAddon) {
172
+ expect(partialAddon.score).toBeLessThan(fullAddon.score);
173
+ }
174
+ });
175
+ test('matchedTokens заполнены для каждой категории', () => {
176
+ const ranked = (0, find_tool_js_1.rankToolCategories)('audit repair', catalog);
177
+ const project = ranked.find(r => r.category === 'project');
178
+ expect(project).toBeDefined();
179
+ expect(project.matchedTokens).toEqual(expect.arrayContaining(['audit', 'repair']));
180
+ });
181
+ test('пустой запрос → пустой результат', () => {
182
+ expect((0, find_tool_js_1.rankToolCategories)('', catalog)).toEqual([]);
183
+ expect((0, find_tool_js_1.rankToolCategories)(' ', catalog)).toEqual([]);
184
+ });
185
+ });
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const fc = __importStar(require("fast-check"));
37
+ const addon_tool_js_1 = require("../tools/addon-tool.js");
38
+ describe('getComponentApi', () => {
39
+ test('известный компонент — возвращает структуру', () => {
40
+ const list = (0, addon_tool_js_1.listComponents)({ limit: 5 });
41
+ const first = list.components[0];
42
+ // Поиск по .name
43
+ const r1 = (0, addon_tool_js_1.getComponentApi)(first.name);
44
+ if (!r1.code) {
45
+ expect(r1.name).toBe(first.name);
46
+ }
47
+ // Поиск по .class
48
+ const r2 = (0, addon_tool_js_1.getComponentApi)(first.class);
49
+ if (!r2.code) {
50
+ expect(r2.class).toBe(first.class);
51
+ }
52
+ });
53
+ test('case-insensitive: "CMSMODEL" вернёт компонент без AMBIGUOUS', () => {
54
+ const lower = (0, addon_tool_js_1.getComponentApi)('cmsmodel');
55
+ const upper = (0, addon_tool_js_1.getComponentApi)('CMSMODEL');
56
+ // Если 'cmsmodel' — единственный компонент, то и upper должен найти.
57
+ if (!lower.code) {
58
+ expect(lower.name).toBeDefined();
59
+ }
60
+ if (!lower.code && !upper.code) {
61
+ expect(upper.name).toBe(lower.name);
62
+ }
63
+ });
64
+ test('частичное совпадение → AMBIGUOUS_COMPONENT без выбора произвольного', () => {
65
+ const list = (0, addon_tool_js_1.listComponents)({ limit: 100 });
66
+ // Найти prefix, который матчит несколько
67
+ const names = list.components.map(c => c.name);
68
+ const classes = list.components.map(c => c.class);
69
+ const all = new Set([...names, ...classes]);
70
+ let chosen;
71
+ let matchesCount = 0;
72
+ for (const candidate of all) {
73
+ const r = (0, addon_tool_js_1.getComponentApi)(candidate);
74
+ if (!r.code) {
75
+ matchesCount += 1;
76
+ if (matchesCount > 1)
77
+ break;
78
+ }
79
+ }
80
+ // Подтверждаем что все кандидаты в candidates при AMBIGUOUS_COMPONENT
81
+ void chosen;
82
+ });
83
+ test('partial "cms" возвращает кандидатов, не один случайный', () => {
84
+ const result = (0, addon_tool_js_1.getComponentApi)('cms');
85
+ if (result.code === 'AMBIGUOUS_COMPONENT') {
86
+ expect(Array.isArray(result.candidates)).toBe(true);
87
+ // candidates содержит все вхождения
88
+ const all = (0, addon_tool_js_1.listComponents)({ limit: 1000 });
89
+ expect(result.candidates.length).toBeGreaterThan(0);
90
+ // Каждый candidate есть в оригинальной базе
91
+ for (const c of result.candidates) {
92
+ const exists = all.components.some(orig => orig.name === c.name && orig.class === c.class);
93
+ expect(exists).toBe(true);
94
+ }
95
+ // Не должно быть дубликатов
96
+ const seen = new Set(result.candidates.map(c => c.name + ':' + c.class));
97
+ expect(seen.size).toBe(result.candidates.length);
98
+ }
99
+ else if (!result.code) {
100
+ // Единственное совпадение
101
+ expect(typeof result.name).toBe('string');
102
+ }
103
+ });
104
+ test('несуществующий компонент → COMPONENT_NOT_FOUND + список available', () => {
105
+ const result = (0, addon_tool_js_1.getComponentApi)('xyzNonExistentComponentName');
106
+ expect(result.code).toBe('COMPONENT_NOT_FOUND');
107
+ expect(Array.isArray(result.available)).toBe(true);
108
+ });
109
+ test('description truncated to 80 chars in COMPONENT_NOT_FOUND', () => {
110
+ const result = (0, addon_tool_js_1.getComponentApi)('xyzNotHere');
111
+ for (const c of result.available) {
112
+ expect(c.description.length).toBeLessThanOrEqual(80);
113
+ }
114
+ });
115
+ test('методы компонента всегда присутствуют для точного совпадения', () => {
116
+ const list = (0, addon_tool_js_1.listComponents)({ limit: 5 });
117
+ for (const c of list.components) {
118
+ const r = (0, addon_tool_js_1.getComponentApi)(c.name);
119
+ if (!r.code) {
120
+ expect(Array.isArray(r.methods)).toBe(true);
121
+ }
122
+ }
123
+ });
124
+ test('property-based: для произвольного запроса возвращается валидный объект', () => {
125
+ fc.assert(fc.property(fc.string({ minLength: 0, maxLength: 200 }), query => {
126
+ const result = (0, addon_tool_js_1.getComponentApi)(query);
127
+ expect(typeof result).toBe('object');
128
+ expect(result).not.toBeNull();
129
+ return true;
130
+ }), { numRuns: 50 });
131
+ });
132
+ test('инъекция в запрос имени не ломает lookup', () => {
133
+ const evilQueries = [
134
+ "'); DROP TABLE components; --",
135
+ '<script>alert(1)</script>',
136
+ "'; rm -rf /; '",
137
+ ];
138
+ for (const q of evilQueries) {
139
+ const result = (0, addon_tool_js_1.getComponentApi)(q);
140
+ expect(result).toBeDefined();
141
+ expect(typeof result).toBe('object');
142
+ }
143
+ });
144
+ });
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const addon_tool_js_1 = require("../tools/addon-tool.js");
4
+ const hooks_tool_js_1 = require("../tools/hooks-tool.js");
5
+ const layout_tool_js_1 = require("../tools/layout-tool.js");
6
+ const scaffold_tool_js_1 = require("../tools/scaffold-tool.js");
7
+ const artifact_tool_js_1 = require("../tools/artifact-tool.js");
8
+ const hooks_tool_js_2 = require("../tools/hooks-tool.js");
9
+ describe('generator hardening', () => {
10
+ test('generated addon passes validation with full package paths', () => {
11
+ const result = (0, scaffold_tool_js_1.scaffoldAddon)({
12
+ name: 'catalog',
13
+ title: 'Каталог',
14
+ type: 'basic',
15
+ });
16
+ const validation = (0, addon_tool_js_1.validateAddon)(result.files);
17
+ expect(validation.errors).toEqual([]);
18
+ expect(validation.is_valid).toBe(true);
19
+ });
20
+ test('escapes XML, INI, and PHP strings', () => {
21
+ const title = `A & <B> 'quoted' "value"`;
22
+ const result = (0, scaffold_tool_js_1.scaffoldAddon)({
23
+ name: 'safe_addon',
24
+ title,
25
+ type: 'basic',
26
+ });
27
+ expect(result.files['package/system/controllers/safe_addon/manifest.xml']).toContain('A &amp; &lt;B&gt; &apos;quoted&apos; &quot;value&quot;');
28
+ expect(result.files['[pkg] manifest.ru.ini']).toContain('\\"value\\"');
29
+ expect(result.files['package/system/languages/ru/controllers/safe_addon/safe_addon.php']).toContain("\\'quoted\\'");
30
+ });
31
+ test('rejects invalid names and versions', () => {
32
+ expect(() => (0, scaffold_tool_js_1.scaffoldAddon)({ name: '../bad', title: 'Bad', type: 'basic' })).toThrow();
33
+ expect(() => (0, scaffold_tool_js_1.scaffoldAddon)({ name: 'valid_name', title: 'Bad', type: 'basic', version: 'latest' })).toThrow();
34
+ expect(() => (0, scaffold_tool_js_1.scaffoldTemplate)({ name: '!', title: 'Bad' })).toThrow();
35
+ });
36
+ test('quotes YAML scalars safely', () => {
37
+ const result = (0, layout_tool_js_1.scaffoldLayoutScheme)({
38
+ rows: [{ title: 'Footer: links #1', cols: [{ title: 'yes', position: 'null' }] }],
39
+ });
40
+ expect(result.yaml).toContain('title: "Footer: links #1"');
41
+ expect(result.yaml).toContain('title: "yes"');
42
+ expect(result.yaml).toContain('name: "null"');
43
+ });
44
+ test('does not silently choose ambiguous lookup results', () => {
45
+ expect((0, hooks_tool_js_1.getHookDetails)('content').code).toBe('AMBIGUOUS_HOOK');
46
+ expect((0, addon_tool_js_1.getComponentApi)('cms').code).toBe('AMBIGUOUS_COMPONENT');
47
+ expect((0, hooks_tool_js_1.searchHooks)('$data').total).toBeGreaterThan(0);
48
+ });
49
+ test('paginates large hook results with an opaque cursor', () => {
50
+ const first = (0, hooks_tool_js_2.listHooks)(undefined, undefined, { limit: 2 });
51
+ const second = (0, hooks_tool_js_2.listHooks)(undefined, undefined, {
52
+ limit: 2,
53
+ cursor: first.page.next_cursor,
54
+ });
55
+ expect(first.page.returned).toBe(2);
56
+ expect(second.hooks).toHaveLength(2);
57
+ });
58
+ test('parses artifacts and round-trips a safe ZIP archive', () => {
59
+ const files = {
60
+ '[pkg] manifest.ru.ini': '[info]\ntitle="Test"',
61
+ 'package/system/controllers/test/manifest.xml': '<addon><name>test</name></addon>',
62
+ 'layout.yaml': 'layout:\n rows: {}',
63
+ };
64
+ expect((0, artifact_tool_js_1.validateGeneratedArtifacts)(files).is_valid).toBe(true);
65
+ const archive = (0, artifact_tool_js_1.buildAddonArchive)(files);
66
+ const inspected = (0, artifact_tool_js_1.inspectAddonArchive)(archive.archive);
67
+ expect(inspected.is_valid).toBe(true);
68
+ expect(inspected.paths).toContain('manifest.ru.ini');
69
+ });
70
+ test('rejects archive traversal paths', () => {
71
+ expect(() => (0, artifact_tool_js_1.buildAddonArchive)({ '../escape.php': '<?php return true;' })).toThrow('Небезопасный путь архива');
72
+ });
73
+ });