Compare commits

...

5 Commits

Author SHA1 Message Date
Jacob Nguyen
18d1c3ba08 unconsole 2025-02-02 19:36:29 -06:00
Jacob Nguyen
9b86063102 fixonwindows 2025-02-02 19:31:30 -06:00
Jacob Nguyen
6bb6581b73 fix 2025-02-02 19:19:38 -06:00
jacob
98026ff3f3 fixinitializing 2025-02-02 10:33:12 -06:00
jacob
a088008175 fix:faster-autocmp 2025-02-01 13:56:34 -06:00
7 changed files with 393 additions and 342 deletions

View File

@@ -20,6 +20,7 @@
"prepare": "tsc", "prepare": "tsc",
"pretty": "prettier --write .", "pretty": "prettier --write .",
"tdd": "vitest", "tdd": "vitest",
"benchmark": "vitest bench",
"test": "vitest --run", "test": "vitest --run",
"analyze-imports": "npx depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg" "analyze-imports": "npx depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg"
}, },

View File

@@ -6,12 +6,12 @@ import type {
MessageContextMenuCommandInteraction, MessageContextMenuCommandInteraction,
ModalSubmitInteraction, ModalSubmitInteraction,
UserContextMenuCommandInteraction, UserContextMenuCommandInteraction,
AutocompleteInteraction AutocompleteInteraction,
} from 'discord.js'; } from 'discord.js';
import { ApplicationCommandOptionType, InteractionType } from 'discord.js'; import { ApplicationCommandOptionType, InteractionType } from 'discord.js';
import { PluginType } from './structures/enums'; import { PluginType } from './structures/enums';
import assert from 'assert';
import type { Payload, UnpackedDependencies } from '../types/utility'; import type { Payload, UnpackedDependencies } from '../types/utility';
import path from 'node:path'
export const createSDT = (module: Module, deps: UnpackedDependencies, params: string|undefined) => { export const createSDT = (module: Module, deps: UnpackedDependencies, params: string|undefined) => {
return { return {
@@ -57,51 +57,31 @@ export function partitionPlugins<T,V>
return [controlPlugins, initPlugins] as [T[], V[]]; return [controlPlugins, initPlugins] as [T[], V[]];
} }
/** export const createLookupTable = (options: SernOptionsData[]): Map<string, SernAutocompleteData> => {
* Uses an iterative DFS to check if an autocomplete node exists on the option tree const table = new Map<string, SernAutocompleteData>();
* @param iAutocomplete _createLookupTable(table, options, "<parent>");
* @param options return table;
*/ }
export function treeSearch(
iAutocomplete: AutocompleteInteraction, const _createLookupTable = (table: Map<string, SernAutocompleteData>, options: SernOptionsData[], parent: string) => {
options: SernOptionsData[] | undefined, for (const opt of options) {
): SernAutocompleteData & { parent?: string } | undefined { const name = path.posix.join(parent, opt.name)
if (options === undefined) return undefined; switch(opt.type) {
//clone to prevent mutation of original command module
const _options = options.map(a => ({ ...a }));
const subcommands = new Set();
while (_options.length > 0) {
const cur = _options.pop()!;
switch (cur.type) {
case ApplicationCommandOptionType.Subcommand: { case ApplicationCommandOptionType.Subcommand: {
subcommands.add(cur.name); _createLookupTable(table, opt.options ?? [], name);
for (const option of cur.options ?? []) _options.push(option); } break;
} break;
case ApplicationCommandOptionType.SubcommandGroup: { case ApplicationCommandOptionType.SubcommandGroup: {
for (const command of cur.options ?? []) _options.push(command); _createLookupTable(table, opt.options ?? [], name);
} break; } break;
default: { default: {
if ('autocomplete' in cur && cur.autocomplete) { if(Reflect.get(opt, 'autocomplete') === true) {
const choice = iAutocomplete.options.getFocused(true); table.set(name, opt as SernAutocompleteData)
assert( 'command' in cur, 'No `command` property found for option ' + cur.name);
if (subcommands.size > 0) {
const parent = iAutocomplete.options.getSubcommand();
const parentAndOptionMatches =
subcommands.has(parent) && cur.name === choice.name;
if (parentAndOptionMatches) {
return { ...cur, parent };
}
} else {
if (cur.name === choice.name) {
return { ...cur, parent: undefined };
}
}
} }
} break; } break;
} }
} }
}
}
interface InteractionTypable { interface InteractionTypable {
type: InteractionType; type: InteractionType;

View File

@@ -1,10 +1,11 @@
import type { Module } from '../types/core-modules' import type { Module, SernAutocompleteData } from '../types/core-modules'
import { callPlugins, executeModule } from './event-utils'; import { callPlugins, executeModule } from './event-utils';
import { SernError } from '../core/structures/enums' import { SernError } from '../core/structures/enums'
import { createSDT, isAutocomplete, isCommand, isContextCommand, isMessageComponent, isModal, resultPayload, treeSearch } from '../core/functions' import { createSDT, isAutocomplete, isCommand, isContextCommand, isMessageComponent, isModal, resultPayload } from '../core/functions'
import type { UnpackedDependencies } from '../types/utility'; import type { UnpackedDependencies } from '../types/utility';
import * as Id from '../core/id' import * as Id from '../core/id'
import { Context } from '../core/structures/context'; import { Context } from '../core/structures/context';
import path from 'node:path';
@@ -31,10 +32,15 @@ export function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: s
let payload; let payload;
// handles autocomplete // handles autocomplete
if(isAutocomplete(event)) { if(isAutocomplete(event)) {
//@ts-ignore stfu const lookupTable = module.locals['@sern/lookup-table'] as Map<string, SernAutocompleteData>
const { command } = treeSearch(event, module.options); const subCommandGroup = event.options.getSubcommandGroup() ?? "",
payload= { module: command as Module, //autocomplete is not a true "module" warning cast! subCommand = event.options.getSubcommand() ?? "",
args: [event, createSDT(command, deps, params)] }; option = event.options.getFocused(true),
fullPath = path.posix.join("<parent>", subCommandGroup, subCommand, option.name)
const resolvedModule = (lookupTable.get(fullPath)!.command) as Module
payload= { module: resolvedModule , //autocomplete is not a true "module" warning cast!
args: [event, createSDT(resolvedModule, deps, params)] };
// either CommandTypes Slash | ContextMessage | ContextUesr // either CommandTypes Slash | ContextMessage | ContextUesr
} else if(isCommand(event)) { } else if(isCommand(event)) {
const sdt = createSDT(module, deps, params) const sdt = createSDT(module, deps, params)

View File

@@ -1,10 +1,11 @@
import * as Files from '../core/module-loading' import * as Files from '../core/module-loading'
import { once } from 'node:events'; import { once } from 'node:events';
import { resultPayload } from '../core/functions'; import { createLookupTable, resultPayload } from '../core/functions';
import { CommandType } from '../core/structures/enums'; import { CommandType } from '../core/structures/enums';
import { Module } from '../types/core-modules'; import { Module, SernOptionsData } from '../types/core-modules';
import type { UnpackedDependencies, Wrapper } from '../types/utility'; import type { UnpackedDependencies, Wrapper } from '../types/utility';
import { callInitPlugins } from './event-utils'; import { callInitPlugins } from './event-utils';
import { SernAutocompleteData } from '..';
export default async function(dirs: string | string[], deps : UnpackedDependencies) { export default async function(dirs: string | string[], deps : UnpackedDependencies) {
const { '@sern/client': client, const { '@sern/client': client,
@@ -28,6 +29,12 @@ export default async function(dirs: string | string[], deps : UnpackedDependenci
throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``); throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``);
} }
const resultModule = await callInitPlugins(module, deps, true); const resultModule = await callInitPlugins(module, deps, true);
if(module.type === CommandType.Both || module.type === CommandType.Slash) {
const options = (Reflect.get(module, 'options') ?? []) as SernOptionsData[];
const lookupTable = createLookupTable(options)
module.locals['@sern/lookup-table'] = lookupTable;
}
// FREEZE! no more writing!! // FREEZE! no more writing!!
commands.set(resultModule.meta.id, Object.freeze(resultModule)); commands.set(resultModule.meta.id, Object.freeze(resultModule));
sEmitter.emit('module.register', resultPayload('success', resultModule)); sEmitter.emit('module.register', resultPayload('success', resultModule));

84
test/autocomp.bench.ts Normal file
View File

@@ -0,0 +1,84 @@
import { describe } from 'node:test'
import { bench } from 'vitest'
import { SernAutocompleteData, SernOptionsData } from '../src'
import { createRandomChoice } from './setup/util'
import { ApplicationCommandOptionType, AutocompleteFocusedOption, AutocompleteInteraction } from 'discord.js'
import { createLookupTable } from '../src/core/functions'
import assert from 'node:assert'
/**
* Uses an iterative DFS to check if an autocomplete node exists on the option tree
* This is the old internal method that sern used to resolve autocomplete
* @param iAutocomplete
* @param options
*/
function treeSearch(
choice: AutocompleteFocusedOption,
parent: string|undefined,
options: SernOptionsData[] | undefined,
): SernAutocompleteData & { parent?: string } | undefined {
if (options === undefined) return undefined;
//clone to prevent mutation of original command module
const _options = options.map(a => ({ ...a }));
const subcommands = new Set();
while (_options.length > 0) {
const cur = _options.pop()!;
switch (cur.type) {
case ApplicationCommandOptionType.Subcommand: {
subcommands.add(cur.name);
for (const option of cur.options ?? []) _options.push(option);
} break;
case ApplicationCommandOptionType.SubcommandGroup: {
for (const command of cur.options ?? []) _options.push(command);
} break;
default: {
if ('autocomplete' in cur && cur.autocomplete) {
assert( 'command' in cur, 'No `command` property found for option ' + cur.name);
if (subcommands.size > 0) {
const parentAndOptionMatches =
subcommands.has(parent) && cur.name === choice.name;
if (parentAndOptionMatches) {
return { ...cur, parent };
}
} else {
if (cur.name === choice.name) {
return { ...cur, parent: undefined };
}
}
}
} break;
}
}
}
const options: SernOptionsData[] = [
createRandomChoice(),
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'autocomplete',
description: 'here',
autocomplete: true,
command: { onEvent: [], execute: () => {} },
},
]
const table = createLookupTable(options)
describe('autocomplete lookup', () => {
bench('lookup table', () => {
table.get('<parent>/autocomplete')
}, { time: 500 })
bench('naive treeSearch', () => {
treeSearch({ focused: true,
name: 'autocomplete',
value: 'autocomplete',
type: ApplicationCommandOptionType.String }, undefined, options)
}, { time: 500 })
})

View File

@@ -1,29 +1,17 @@
//@ts-nocheck //@ts-nocheck
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
import { PluginType, SernOptionsData, controller } from '../../src/index'; import { PluginType, SernOptionsData, controller } from '../../src/index';
import { partitionPlugins, treeSearch } from '../../src/core/functions'; import { createLookupTable, partitionPlugins, treeSearch } from '../../src/core/functions';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js'; import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
import { createRandomChoice, createRandomPlugins } from '../setup/util';
describe('functions', () => { describe('functions', () => {
afterEach(() => { afterEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
function createRandomPlugins(len: number) {
const random = () => Math.floor(Math.random() * 2) + 1; // 1 or 2, plugin enum
return Array.from({ length: len }, () => ({
type: random(),
execute: () => (random() === 1 ? controller.next() : controller.stop()),
}));
}
function createRandomChoice() {
return {
type: faker.number.int({ min: 1, max: 11 }),
name: faker.word.noun(),
description: faker.word.adjective(),
};
}
it('should partition plugins correctly', () => { it('should partition plugins correctly', () => {
const plugins = createRandomPlugins(100); const plugins = createRandomPlugins(100);
const [onEvent, init] = partitionPlugins(plugins); const [onEvent, init] = partitionPlugins(plugins);
@@ -32,308 +20,275 @@ describe('functions', () => {
for (const el of init) expect(el.type).to.equal(PluginType.Init); for (const el of init) expect(el.type).to.equal(PluginType.Init);
}); });
it('should tree search options tree depth 1', () => { describe('autocomplete', ( ) => {
//@ts-expect-error mocking
let autocmpInteraction = new AutocompleteInteraction('autocomplete'); it('should tree search options tree depth 1', () => {
const options: SernOptionsData[] = [ const options: SernOptionsData[] = [
createRandomChoice(), createRandomChoice(),
createRandomChoice(), {
createRandomChoice(), type: ApplicationCommandOptionType.String,
{ name: 'autocomplete',
type: ApplicationCommandOptionType.String, description: 'here',
name: 'autocomplete', autocomplete: true,
description: 'here', command: { onEvent: [], execute: vi.fn() },
autocomplete: true, },
command: { onEvent: [], execute: vi.fn() }, ];
}, const table = createLookupTable(options)
]; const result = table.get('<parent>/autocomplete')
autocmpInteraction.options.getFocused.mockReturnValue({ expect(result == undefined).to.be.false;
name: 'autocomplete', expect(result.name).to.be.eq('autocomplete');
value: faker.string.alpha(), expect(result.command).to.be.not.undefined;
focused: true, }),
}); it('should tree search depth 2', () => {
const result = treeSearch(autocmpInteraction, options); const subcommandName = faker.string.alpha();
expect(result == undefined).to.be.false; const options: SernOptionsData[] = [
expect(result.name).to.be.eq('autocomplete'); {
expect(result.command).to.be.not.undefined; type: ApplicationCommandOptionType.Subcommand,
}), name: subcommandName,
it('should tree search depth 2', () => { description: faker.string.alpha(),
//@ts-expect-error mocking options: [
let autocmpInteraction = new AutocompleteInteraction('nested'); createRandomChoice(),
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
];
const table = createLookupTable(options)
const result = table.get(`<parent>/${subcommandName}/nested`)
expect(result == undefined).to.be.false;
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
});
it('should tree search depth n > 2', () => {
const subgroupName = faker.string.alpha()
const subcommandName = faker.string.alpha(); const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [ const options: SernOptionsData[] = [
{ {
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.SubcommandGroup,
name: subcommandName, name: subgroupName,
description: faker.string.alpha(), description: faker.string.alpha(),
options: [ options: [
createRandomChoice(),
createRandomChoice(),
createRandomChoice(),
{ {
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.Subcommand,
name: 'nested', name: subcommandName,
description: faker.string.alpha(), description: faker.string.alpha(),
autocomplete: true, options: [
command: { createRandomChoice(),
onEvent: [], createRandomChoice(),
execute: () => {}, {
}, type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
createRandomChoice(),
],
}, },
], ],
}, },
]; ];
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName); const table = createLookupTable(options)
autocmpInteraction.options.getFocused.mockReturnValue({ const result = table.get(`<parent>/${subgroupName}/${subcommandName}/nested`)
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
const result = treeSearch(autocmpInteraction, options);
expect(result == undefined).to.be.false; expect(result == undefined).to.be.false;
expect(result.name).to.be.eq('nested'); expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined; expect(result.command).to.be.not.undefined;
}); });
it('should tree search depth n > 2', () => { it('should correctly resolve suboption of the same name given two subcommands ', () => {
//@ts-expect-error mocking const subcommandName = faker.string.alpha();
let autocmpInteraction = new AutocompleteInteraction('nested'); const groupname = faker.string.alpha()
const subcommandName = faker.string.alpha(); const options: SernOptionsData[] = [
const options: SernOptionsData[] = [ {
{ type: ApplicationCommandOptionType.SubcommandGroup,
type: ApplicationCommandOptionType.SubcommandGroup, name: groupname,
name: faker.string.alpha(), description: faker.string.alpha(),
description: faker.string.alpha(), options: [
options: [ {
{ type: ApplicationCommandOptionType.Subcommand,
type: ApplicationCommandOptionType.Subcommand, name: subcommandName,
name: subcommandName, description: faker.string.alpha(),
description: faker.string.alpha(), options: [
options: [ createRandomChoice(),
createRandomChoice(), createRandomChoice(),
createRandomChoice(), {
{ type: ApplicationCommandOptionType.String,
type: ApplicationCommandOptionType.String, name: 'nested',
name: 'nested', description: faker.string.alpha(),
description: faker.string.alpha(), autocomplete: true,
autocomplete: true, command: {
command: { onEvent: [],
onEvent: [], execute: () => {},
execute: () => {}, },
}, },
}, ],
createRandomChoice(), },
], {
}, type: ApplicationCommandOptionType.Subcommand,
], name: subcommandName + 'a',
}, description: faker.string.alpha(),
]; options: [
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName); createRandomChoice(),
autocmpInteraction.options.getFocused.mockReturnValue({ {
name: 'nested', type: ApplicationCommandOptionType.String,
value: faker.string.alpha(), name: 'nested',
focused: true, description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
],
},
];
const table = createLookupTable(options)
const result = table.get(`<parent>/${groupname}/${subcommandName}/nested`);
expect(result).toBeTruthy();
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
}); });
const result = treeSearch(autocmpInteraction, options); it('two subcommands with an option of the same name', () => {
expect(result == undefined).to.be.false; const groupName = faker.string.alpha()
expect(result.name).to.be.eq('nested'); const subcommandName = faker.string.alpha();
expect(result.command).to.be.not.undefined; const options: SernOptionsData[] = [
}); {
it('should correctly resolve suboption of the same name given two subcommands ', () => { type: ApplicationCommandOptionType.SubcommandGroup,
let autocmpInteraction = new AutocompleteInteraction('nested'); name: groupName,
const subcommandName = faker.string.alpha(); description: faker.string.alpha(),
const options: SernOptionsData[] = [ options: [
{ {
type: ApplicationCommandOptionType.SubcommandGroup, type: ApplicationCommandOptionType.Subcommand,
name: faker.string.alpha(), name: subcommandName,
description: faker.string.alpha(), description: faker.string.alpha(),
options: [ options: [
{ createRandomChoice(),
type: ApplicationCommandOptionType.Subcommand, createRandomChoice(),
name: subcommandName, {
description: faker.string.alpha(), type: ApplicationCommandOptionType.String,
options: [ name: 'nested',
createRandomChoice(), description: faker.string.alpha(),
createRandomChoice(), autocomplete: true,
{ command: {
type: ApplicationCommandOptionType.String, onEvent: [],
name: 'nested', execute: () => {},
description: faker.string.alpha(), },
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
}, },
}, ],
], },
}, {
{ type: ApplicationCommandOptionType.Subcommand,
type: ApplicationCommandOptionType.Subcommand, name: subcommandName + 'anothera',
name: subcommandName + 'a', description: faker.string.alpha(),
description: faker.string.alpha(), options: [
options: [ createRandomChoice(),
createRandomChoice(), {
{ type: ApplicationCommandOptionType.String,
type: ApplicationCommandOptionType.String, name: 'nested',
name: 'nested', description: faker.string.alpha(),
description: faker.string.alpha(), autocomplete: true,
autocomplete: true, command: {
command: { onEvent: [],
onEvent: [], execute: () => {},
execute: () => {}, },
}, },
}, ],
], },
}, ],
], },
}, ];
];
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName); const table = createLookupTable(options)
autocmpInteraction.options.getFocused.mockReturnValue({ const result = table.get(`<parent>/${groupName}/${subcommandName}/nested`);
name: 'nested', expect(result).toBeTruthy();
value: faker.string.alpha(), expect(result.name).to.be.eq('nested');
focused: true, expect(result.command).to.be.not.undefined;
});
const result = treeSearch(autocmpInteraction, options);
expect(result).toBeTruthy();
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
});
it('two subcommands with an option of the same name', () => {
let autocmpInteraction = new AutocompleteInteraction('nested');
const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [
{
type: ApplicationCommandOptionType.SubcommandGroup,
name: faker.string.alpha(),
description: faker.string.alpha(),
options: [
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName,
description: faker.string.alpha(),
options: [
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName + 'a',
description: faker.string.alpha(),
options: [
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
],
},
];
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName);
autocmpInteraction.options.getFocused.mockReturnValue({
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
const result = treeSearch(autocmpInteraction, options);
expect(result).toBeTruthy();
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
let autocmpInteraction2 = new AutocompleteInteraction('nested');
autocmpInteraction2.options.getSubcommand.mockReturnValue(subcommandName + 'a');
autocmpInteraction2.options.getFocused.mockReturnValue({
name: 'nested',
value: faker.string.alpha(),
focused: true,
}); });
const result2 = treeSearch(autocmpInteraction2, options);
expect(result2).toBeTruthy();
expect(result2?.name).toEqual('nested');
});
it('simulates autocomplete typing and resolution', () => { it('simulates autocomplete typing and resolution', () => {
const subcommandName = faker.string.alpha(); const subcommandGroupName = faker.string.alpha()
const optionName = faker.word.noun(); const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [ const optionName = faker.word.noun();
{ const options: SernOptionsData[] = [
type: ApplicationCommandOptionType.SubcommandGroup, {
name: faker.string.alpha(), type: ApplicationCommandOptionType.SubcommandGroup,
description: faker.string.alpha(), name: subcommandGroupName,
options: [ description: faker.string.alpha(),
{ options: [
type: ApplicationCommandOptionType.Subcommand, {
name: subcommandName, type: ApplicationCommandOptionType.Subcommand,
description: faker.string.alpha(), name: subcommandName,
options: [ description: faker.string.alpha(),
createRandomChoice(), options: [
createRandomChoice(), createRandomChoice(),
{ createRandomChoice(),
type: ApplicationCommandOptionType.String, {
name: optionName, type: ApplicationCommandOptionType.String,
description: faker.string.alpha(), name: optionName,
autocomplete: true, description: faker.string.alpha(),
command: { autocomplete: true,
onEvent: [], command: {
execute: vi.fn(), onEvent: [],
execute: vi.fn(),
},
}, },
}, ],
], },
}, {
{ type: ApplicationCommandOptionType.Subcommand,
type: ApplicationCommandOptionType.Subcommand, name: subcommandName + 'a',
name: subcommandName + 'a', description: faker.string.alpha(),
description: faker.string.alpha(), options: [
options: [ createRandomChoice(),
createRandomChoice(), {
{ type: ApplicationCommandOptionType.String,
type: ApplicationCommandOptionType.String, name: optionName,
name: optionName, description: faker.string.alpha(),
description: faker.string.alpha(), autocomplete: true,
autocomplete: true, command: {
command: { onEvent: [],
onEvent: [], execute: vi.fn(),
execute: vi.fn(), },
}, },
}, ],
], },
}, ],
], },
}, ];
]; let accumulator = '';
let accumulator = ''; let result: unknown;
let result: unknown; const table = createLookupTable(options)
for (const char of optionName) { for (const char of optionName) {
accumulator += char; accumulator += char;
const focusedValue = {
name: accumulator,
value: faker.string.alpha(),
focused: true,
};
result = table.get(`<parent>/${subcommandGroupName}/${subcommandName}/${focusedValue.name}` );
}
expect(result).toBeTruthy();
});
})
const autocomplete = new AutocompleteInteraction(accumulator);
autocomplete.options.getSubcommand.mockReturnValue(subcommandName);
autocomplete.options.getFocused.mockReturnValue({
name: accumulator,
value: faker.string.alpha(),
focused: true,
});
result = treeSearch(autocomplete, options);
}
expect(result).toBeTruthy();
});
}); });

View File

@@ -28,3 +28,21 @@ export function createRandomModule(plugins: any[]): Processed<Module> {
execute: vi.fn(), execute: vi.fn(),
}; };
} }
export function createRandomChoice() {
return {
type: faker.number.int({ min: 1, max: 11 }),
name: faker.word.noun(),
description: faker.word.adjective(),
};
}
export function createRandomPlugins(len: number) {
const random = () => Math.floor(Math.random() * 2) + 1; // 1 or 2, plugin enum
return Array.from({ length: len }, () => ({
type: random(),
execute: () => (random() === 1 ? controller.next() : controller.stop()),
}));
}