import { parseSchemaDir } from '@mt-tl/tl' /** * Build a JSON Schema (draft-07) for scenario YAML files, off the consumer's TL * schema. Editors (VS Code + the YAML extension) use it for autocomplete + * linting: the scenario structure, and the `invoke:` method name (an enum of the * TL method names). Ship via the `mtproto-test schema` command — consumers don't * hand-write it. * * Param shapes per method are NOT modelled (`params` is free-form); `${...}` * refs are free-form strings. * * @param schemaDirs - the app's `.tl` schema directory (or several). */ export function generateScenarioSchema(schemaDirs: string | string[]): object { const dirs = Array.isArray(schemaDirs) ? schemaDirs : [schemaDirs] const defs = dirs.flatMap(d => parseSchemaDir(d).defs) const methods = [...new Set(defs.filter(d => d.kind === 'method').map(d => d.name))].sort() const constructors = [...new Set(defs.filter(d => d.kind !== 'method').map(d => d.name))].sort() return { $schema: 'http://json-schema.org/draft-07/schema#', title: 'mtproto-test scenario', description: 'A scenario for `mtproto-test run`. Generated by `mtproto-test schema`.', type: 'object', required: ['target', 'steps'], additionalProperties: false, properties: { target: { type: 'object', required: ['url'], additionalProperties: false, properties: { url: { type: 'string', description: 'ws:// | wss:// | tcp://host:port' }, transport: { enum: ['ws', 'tcp'], description: 'Override; otherwise inferred from the URL scheme.' }, schema: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Business .tl dir(s); resolved from the run dir, then this file.', }, publicKey: { type: 'string', description: "The stand's RSA public-key PEM." }, layer: { type: 'integer', description: 'Negotiate this TL layer via invokeWithLayer(initConnection).' }, initConnection: { type: 'object', description: 'Override initConnection fields (api_id, device_model…).' }, defaultTimeoutMs: { type: 'integer' }, }, }, vars: { type: 'object', description: 'Free variables for ${...} interpolation.' }, users: { type: 'object', description: 'Named users, each authenticated before the steps run.', additionalProperties: { type: 'object', additionalProperties: false, properties: { auth: { type: 'object', additionalProperties: false, properties: { recipe: { type: 'string', description: 'Recipe name from the --recipes module.' }, with: { type: 'object', description: 'Args passed to the recipe (interpolated).' }, steps: { type: 'array', items: { $ref: '#/definitions/step' } }, }, }, layer: { type: 'integer', description: 'TL layer for THIS user (overrides target.layer).' }, initConnection: { type: 'object', description: 'Override initConnection fields for this user.' }, }, }, }, steps: { type: 'array', items: { $ref: '#/definitions/step' } }, }, definitions: { method: { type: 'string', description: 'A TL method (from the schema).', enum: methods }, matcher: { description: 'A bare constructor name, or a map of dotted-path → expected value.', oneOf: [{ type: 'string', enum: constructors }, { type: 'object' }], }, step: { type: 'object', additionalProperties: false, oneOf: [{ required: ['invoke'] }, { required: ['expectUpdate'] }, { required: ['recipe'] }], properties: { as: { type: 'string', description: 'Which user runs this step.' }, label: { type: 'string' }, invoke: { $ref: '#/definitions/method' }, params: { type: 'object', description: 'Call params (interpolated).' }, recipe: { type: 'string', description: 'Run a named recipe (a reusable multi-step macro).' }, with: { type: 'object', description: 'Args passed to the recipe (interpolated).' }, expect: { $ref: '#/definitions/matcher' }, expectError: { type: 'object', additionalProperties: false, properties: { code: { type: 'integer' }, message: { type: 'string' } }, }, expectUpdate: { $ref: '#/definitions/matcher' }, nonBlocking: { type: 'boolean', description: "expectUpdate only: don't block — register the expectation and proceed; it's checked after all steps run (order-independent).", }, capture: { type: 'object', additionalProperties: { type: 'string' }, description: "{ 'scope.path': 'result.path' }", }, timeoutMs: { type: 'integer' }, }, }, }, } }